2026-02-17 01:26:58 +09:00
|
|
|
declare const require: (name: string) => any
|
2026-04-03 11:51:19 +10:00
|
|
|
const { describe, test, expect, beforeEach, afterEach, spyOn, mock, vi } = require("bun:test")
|
2026-02-17 01:26:58 +09:00
|
|
|
import { resolveSubagentExecution } from "./subagent-resolver"
|
|
|
|
|
import type { DelegateTaskArgs } from "./types"
|
|
|
|
|
import type { ExecutorContext } from "./executor-types"
|
|
|
|
|
import * as logger from "../../shared/logger"
|
2026-03-03 00:31:12 +09:00
|
|
|
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
|
2026-04-04 07:23:12 +10:00
|
|
|
import * as agentLoader from "../../features/claude-code-agent-loader"
|
2026-04-03 11:51:19 +10:00
|
|
|
|
2026-02-17 01:26:58 +09:00
|
|
|
function createBaseArgs(overrides?: Partial<DelegateTaskArgs>): DelegateTaskArgs {
|
|
|
|
|
return {
|
|
|
|
|
description: "Run review",
|
|
|
|
|
prompt: "Review the current changes",
|
|
|
|
|
run_in_background: false,
|
|
|
|
|
load_skills: [],
|
|
|
|
|
subagent_type: "oracle",
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 20:43:55 +01:00
|
|
|
function createExecutorContext(
|
|
|
|
|
agentsFn: () => Promise<unknown>,
|
|
|
|
|
overrides?: Partial<ExecutorContext>,
|
|
|
|
|
): ExecutorContext {
|
2026-02-17 01:26:58 +09:00
|
|
|
const client = {
|
|
|
|
|
app: {
|
|
|
|
|
agents: agentsFn,
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["client"]
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
client,
|
|
|
|
|
manager: {} as ExecutorContext["manager"],
|
|
|
|
|
directory: "/tmp/test",
|
2026-03-04 20:43:55 +01:00
|
|
|
...overrides,
|
2026-02-17 01:26:58 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("resolveSubagentExecution", () => {
|
|
|
|
|
let logSpy: ReturnType<typeof spyOn> | undefined
|
2026-04-04 07:23:12 +10:00
|
|
|
let mockLoadUserAgents: ReturnType<typeof spyOn>
|
|
|
|
|
let mockLoadProjectAgents: ReturnType<typeof spyOn>
|
2026-02-17 01:26:58 +09:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mock.restore()
|
|
|
|
|
logSpy = spyOn(logger, "log").mockImplementation(() => {})
|
2026-04-04 07:23:12 +10:00
|
|
|
mockLoadUserAgents = spyOn(agentLoader, "loadUserAgents").mockReturnValue({})
|
|
|
|
|
mockLoadProjectAgents = spyOn(agentLoader, "loadProjectAgents").mockReturnValue({})
|
2026-02-17 01:26:58 +09:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
logSpy?.mockRestore()
|
2026-04-04 07:23:12 +10:00
|
|
|
mockLoadUserAgents?.mockRestore()
|
|
|
|
|
mockLoadProjectAgents?.mockRestore()
|
2026-02-17 01:26:58 +09:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns delegation error when agent discovery fails instead of silently proceeding", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const resolverError = new Error("agents API unavailable")
|
|
|
|
|
const args = createBaseArgs()
|
|
|
|
|
const executorCtx = createExecutorContext(async () => {
|
|
|
|
|
throw resolverError
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.agentToUse).toBe("")
|
|
|
|
|
expect(result.categoryModel).toBeUndefined()
|
|
|
|
|
expect(result.error).toBe("Failed to delegate to agent \"oracle\": agents API unavailable")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("logs failure details when subagent resolution throws", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "review" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => {
|
|
|
|
|
throw new Error("network timeout")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(logSpy).toHaveBeenCalledTimes(1)
|
|
|
|
|
const callArgs = logSpy?.mock.calls[0]
|
|
|
|
|
expect(callArgs?.[0]).toBe("[delegate-task] Failed to resolve subagent execution")
|
|
|
|
|
expect(callArgs?.[1]).toEqual({
|
|
|
|
|
requestedAgent: "review",
|
|
|
|
|
parentAgent: "sisyphus",
|
|
|
|
|
error: "network timeout",
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-03-03 00:31:12 +09:00
|
|
|
|
|
|
|
|
test("normalizes matched agent model string before returning categoryModel", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
2026-03-27 15:50:16 +09:00
|
|
|
models: { openai: ["grok-3", "gpt-5.3-codex"] },
|
2026-03-03 00:31:12 +09:00
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "oracle" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => ([
|
|
|
|
|
{ name: "oracle", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
|
|
|
|
]))
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" })
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
})
|
2026-03-04 20:43:55 +01:00
|
|
|
|
|
|
|
|
test("uses agent override fallback_models for subagent runtime fallback chain", async () => {
|
|
|
|
|
//#given
|
2026-03-09 13:11:03 +09:00
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { quotio: ["claude-haiku-4-5"] },
|
|
|
|
|
connected: ["quotio"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
2026-03-04 20:43:55 +01:00
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: ["quotio/gpt-5.2", "glm-5(max)"],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.fallbackChain).toEqual([
|
|
|
|
|
{ providers: ["quotio"], model: "gpt-5.2", variant: undefined },
|
|
|
|
|
{ providers: ["quotio"], model: "glm-5", variant: "max" },
|
|
|
|
|
])
|
2026-03-09 13:11:03 +09:00
|
|
|
cacheSpy.mockRestore()
|
2026-03-04 20:43:55 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("uses category fallback_models when agent override points at category", async () => {
|
|
|
|
|
//#given
|
2026-03-09 13:11:03 +09:00
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { anthropic: ["claude-haiku-4-5"] },
|
|
|
|
|
connected: ["anthropic"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
2026-03-04 20:43:55 +01:00
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
category: "research",
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
userCategories: {
|
|
|
|
|
research: {
|
|
|
|
|
fallback_models: ["anthropic/claude-haiku-4-5"],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["userCategories"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.fallbackChain).toEqual([
|
|
|
|
|
{ providers: ["anthropic"], model: "claude-haiku-4-5", variant: undefined },
|
|
|
|
|
])
|
2026-03-09 13:11:03 +09:00
|
|
|
cacheSpy.mockRestore()
|
2026-03-04 20:43:55 +01:00
|
|
|
})
|
2026-03-18 14:21:27 +01:00
|
|
|
|
|
|
|
|
test("promotes object-style fallback model settings to categoryModel when subagent fallback becomes initial model", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5-unavailable" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4 high",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
temperature: 0.2,
|
|
|
|
|
top_p: 0.8,
|
|
|
|
|
maxTokens: 2048,
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-5.4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
temperature: 0.2,
|
|
|
|
|
top_p: 0.8,
|
|
|
|
|
maxTokens: 2048,
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
2026-03-25 10:49:10 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("does not apply object-style fallback settings when the subagent primary model matches directly", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4-preview"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "openai/gpt-5.4-preview" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-5.4-preview",
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
2026-03-18 14:21:27 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("matches promoted fallback settings after fuzzy model resolution", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4-preview"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5-unavailable" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
temperature: 0.3,
|
|
|
|
|
top_p: 0.4,
|
|
|
|
|
maxTokens: 2222,
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-5.4-preview",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
temperature: 0.3,
|
|
|
|
|
top_p: 0.4,
|
|
|
|
|
maxTokens: 2222,
|
|
|
|
|
thinking: { type: "disabled" },
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("prefers exact promoted fallback match over earlier fuzzy prefix match", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4-preview"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5-unavailable" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4-preview",
|
|
|
|
|
variant: "max",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-5.4-preview",
|
|
|
|
|
variant: "max",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("matches promoted fallback settings when fuzzy resolution extends configured model without hyphen", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4o"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5-unavailable" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-5.4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-5.4o",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-27 15:50:16 +09:00
|
|
|
test("does not use unavailable matchedAgent.model as fallback for custom subagent", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { minimaxi: ["MiniMax-M2.7"] },
|
|
|
|
|
connected: ["minimaxi"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-custom-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "my-custom-agent", mode: "subagent", model: "minimaxi/MiniMax-M2.7-highspeed" },
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel?.modelID).not.toBe("MiniMax-M2.7-highspeed")
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("uses matchedAgent.model as fallback when model is available", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { minimaxi: ["MiniMax-M2.7-highspeed"] },
|
|
|
|
|
connected: ["minimaxi"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-custom-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "my-custom-agent", mode: "subagent", model: "minimaxi/MiniMax-M2.7-highspeed" },
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({ providerID: "minimaxi", modelID: "MiniMax-M2.7-highspeed" })
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-18 14:21:27 +01:00
|
|
|
test("prefers the most specific prefix match when fallback entries share a prefix", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-4o-preview"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(
|
|
|
|
|
async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "quotio/claude-haiku-4-5-unavailable" },
|
|
|
|
|
]),
|
|
|
|
|
{
|
|
|
|
|
agentOverrides: {
|
|
|
|
|
explore: {
|
|
|
|
|
fallback_models: [
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-4",
|
|
|
|
|
variant: "low",
|
|
|
|
|
reasoningEffort: "medium",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
model: "openai/gpt-4o",
|
|
|
|
|
variant: "max",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
} as ExecutorContext["agentOverrides"],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.categoryModel).toEqual({
|
|
|
|
|
providerID: "openai",
|
|
|
|
|
modelID: "gpt-4o-preview",
|
|
|
|
|
variant: "max",
|
|
|
|
|
reasoningEffort: "high",
|
|
|
|
|
})
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
2026-04-03 11:51:19 +10:00
|
|
|
|
|
|
|
|
test("resolves user agent from loadUserAgents when calling task(subagent_type=...)", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
|
|
|
|
|
mockLoadUserAgents.mockReturnValue({
|
|
|
|
|
"my-user-agent": {
|
|
|
|
|
description: "A user agent",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
prompt: "Do something",
|
|
|
|
|
model: "openai/gpt-5.4",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
mockLoadProjectAgents.mockReturnValue({})
|
|
|
|
|
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-user-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => [])
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.agentToUse).toBe("my-user-agent")
|
|
|
|
|
expect(result.categoryModel?.modelID).toBe("gpt-5.4")
|
|
|
|
|
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("resolves project agent from loadProjectAgents when calling task(subagent_type=...)", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { anthropic: ["claude-sonnet-4"] },
|
|
|
|
|
connected: ["anthropic"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
|
|
|
|
|
|
|
|
|
|
mockLoadUserAgents.mockReturnValue({})
|
|
|
|
|
mockLoadProjectAgents.mockReturnValue({
|
|
|
|
|
"my-project-agent": {
|
|
|
|
|
description: "A project agent",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
prompt: "Do project work",
|
|
|
|
|
model: "anthropic/claude-sonnet-4",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-project-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => [])
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.agentToUse).toBe("my-project-agent")
|
|
|
|
|
expect(result.categoryModel?.modelID).toBe("claude-sonnet-4")
|
|
|
|
|
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("server agent takes precedence over user agent with same name", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { openai: ["gpt-5.4"] },
|
|
|
|
|
connected: ["openai"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
|
|
|
|
|
|
|
|
|
mockLoadUserAgents.mockReturnValue({
|
|
|
|
|
"explore": {
|
|
|
|
|
description: "User explore agent",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
prompt: "User prompt",
|
|
|
|
|
model: "openai/gpt-3.5",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
mockLoadProjectAgents.mockReturnValue({})
|
|
|
|
|
|
|
|
|
|
// Server has "explore" agent
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "explore" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => ([
|
|
|
|
|
{ name: "explore", mode: "subagent", model: "openai/gpt-5.4" },
|
|
|
|
|
]))
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.agentToUse).toBe("explore")
|
|
|
|
|
// Should use server's model, not user's
|
|
|
|
|
expect(result.categoryModel?.modelID).toBe("gpt-5.4")
|
|
|
|
|
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-03 11:51:20 +10:00
|
|
|
test("project agent takes precedence over user agent with same name", async () => {
|
|
|
|
|
//#given
|
2026-04-03 11:51:22 +10:00
|
|
|
const cacheSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue({
|
|
|
|
|
models: { minimaxi: ["MiniMax-M2.7-highspeed", "claude-3-haiku"] },
|
|
|
|
|
connected: ["minimaxi"],
|
|
|
|
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
|
|
|
|
})
|
|
|
|
|
const connectedSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["minimaxi"])
|
|
|
|
|
|
2026-04-03 11:51:20 +10:00
|
|
|
mockLoadUserAgents.mockReturnValue({
|
|
|
|
|
"my-custom-agent": {
|
|
|
|
|
description: "User agent",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
prompt: "User prompt",
|
|
|
|
|
model: "minimaxi/claude-3-haiku",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
mockLoadProjectAgents.mockReturnValue({
|
|
|
|
|
"my-custom-agent": {
|
|
|
|
|
description: "Project agent",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
prompt: "Project prompt",
|
|
|
|
|
model: "minimaxi/MiniMax-M2.7-highspeed",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-custom-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => [])
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toBeUndefined()
|
|
|
|
|
expect(result.agentToUse).toBe("my-custom-agent")
|
2026-04-03 11:51:22 +10:00
|
|
|
expect(result.categoryModel?.modelID).toBe("MiniMax-M2.7-highspeed")
|
|
|
|
|
|
|
|
|
|
cacheSpy.mockRestore()
|
|
|
|
|
connectedSpy.mockRestore()
|
2026-04-03 11:51:20 +10:00
|
|
|
})
|
|
|
|
|
|
2026-04-03 11:51:19 +10:00
|
|
|
test("filters out primary agents from user/project when resolving", async () => {
|
|
|
|
|
//#given
|
|
|
|
|
mockLoadUserAgents.mockReturnValue({
|
|
|
|
|
"my-primary-agent": {
|
|
|
|
|
description: "A primary agent",
|
|
|
|
|
mode: "primary",
|
|
|
|
|
prompt: "I am primary",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
mockLoadProjectAgents.mockReturnValue({})
|
|
|
|
|
|
|
|
|
|
const args = createBaseArgs({ subagent_type: "my-primary-agent" })
|
|
|
|
|
const executorCtx = createExecutorContext(async () => [])
|
|
|
|
|
|
|
|
|
|
//#when
|
|
|
|
|
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
|
|
|
|
|
|
|
|
|
//#then
|
|
|
|
|
expect(result.error).toContain("Unknown agent")
|
|
|
|
|
expect(result.agentToUse).toBe("")
|
|
|
|
|
})
|
2026-02-17 01:26:58 +09:00
|
|
|
})
|