Merge branch 'dev' into fix/user-agents-callable-v2
Resolves conflicts with ZWSP agent ordering, display name unification, and test file migration to zauc-mocks split.
This commit is contained in:
@@ -0,0 +1,949 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
import type { DelegateTaskArgs } from "../types"
|
||||
import type { ExecutorContext } from "../executor-types"
|
||||
|
||||
type SubagentResolverModule = typeof import("../subagent-resolver")
|
||||
|
||||
const logMock = mock((..._args: unknown[]) => {})
|
||||
|
||||
const readConnectedProvidersCacheMock = mock(() => null as string[] | null)
|
||||
const readProviderModelsCacheMock = mock(
|
||||
() => null as {
|
||||
models: Record<string, string[]>
|
||||
connected: string[]
|
||||
updatedAt: string
|
||||
} | null,
|
||||
)
|
||||
|
||||
type ClaudeCodeAgentRecord = Record<
|
||||
string,
|
||||
{
|
||||
description?: string
|
||||
mode?: string
|
||||
prompt?: string
|
||||
model?: string | { providerID: string; modelID: string }
|
||||
}
|
||||
>
|
||||
|
||||
const loadUserAgentsMock = mock((): ClaudeCodeAgentRecord => ({}))
|
||||
const loadProjectAgentsMock = mock((_directory?: string): ClaudeCodeAgentRecord => ({}))
|
||||
|
||||
async function importFreshSubagentResolverModule(): Promise<SubagentResolverModule> {
|
||||
return await import(`../subagent-resolver?test=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
function createExecutorContext(
|
||||
agentsFn: () => Promise<unknown>,
|
||||
overrides?: Partial<ExecutorContext>,
|
||||
): ExecutorContext {
|
||||
const client = {
|
||||
app: {
|
||||
agents: agentsFn,
|
||||
},
|
||||
} as ExecutorContext["client"]
|
||||
|
||||
return {
|
||||
client,
|
||||
manager: {} as ExecutorContext["manager"],
|
||||
directory: "/tmp/test",
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe("resolveSubagentExecution", () => {
|
||||
let resolveSubagentExecution: SubagentResolverModule["resolveSubagentExecution"]
|
||||
|
||||
beforeEach(async () => {
|
||||
mock.restore()
|
||||
logMock.mockClear()
|
||||
readConnectedProvidersCacheMock.mockReset()
|
||||
readProviderModelsCacheMock.mockReset()
|
||||
readConnectedProvidersCacheMock.mockReturnValue(null)
|
||||
readProviderModelsCacheMock.mockReturnValue(null)
|
||||
loadUserAgentsMock.mockReset()
|
||||
loadProjectAgentsMock.mockReset()
|
||||
loadUserAgentsMock.mockImplementation(() => ({}))
|
||||
loadProjectAgentsMock.mockImplementation(() => ({}))
|
||||
mock.module("../../../shared/logger", () => ({
|
||||
log: logMock,
|
||||
}))
|
||||
mock.module("../../../shared/connected-providers-cache", () => ({
|
||||
readConnectedProvidersCache: readConnectedProvidersCacheMock,
|
||||
readProviderModelsCache: readProviderModelsCacheMock,
|
||||
hasConnectedProvidersCache: () => readConnectedProvidersCacheMock() !== null,
|
||||
hasProviderModelsCache: () => readProviderModelsCacheMock() !== null,
|
||||
_resetMemCacheForTesting: () => {},
|
||||
}))
|
||||
mock.module("../../../features/claude-code-agent-loader/loader", () => ({
|
||||
loadUserAgents: loadUserAgentsMock,
|
||||
loadProjectAgents: loadProjectAgentsMock,
|
||||
}))
|
||||
mock.module("../../../features/claude-code-agent-loader", () => ({
|
||||
loadUserAgents: loadUserAgentsMock,
|
||||
loadProjectAgents: loadProjectAgentsMock,
|
||||
}))
|
||||
;({ resolveSubagentExecution } = await importFreshSubagentResolverModule())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
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("returns delegation error when subagent resolution throws", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "review" })
|
||||
const executorCtx = createExecutorContext(async () => {
|
||||
throw new Error("network timeout")
|
||||
})
|
||||
|
||||
//#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 "review": network timeout')
|
||||
})
|
||||
|
||||
test("hides primary agents from task delegation lookups", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "sisyphus" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "sisyphus", mode: "primary" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
{ name: "metis", mode: "all" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "sisyphus". Available agents: metis, oracle')
|
||||
})
|
||||
|
||||
test("requires explicit all or subagent mode for task-callable agents", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "custom-worker" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "custom-worker" },
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.agentToUse).toBe("")
|
||||
expect(result.categoryModel).toBeUndefined()
|
||||
expect(result.error).toBe('Unknown agent: "custom-worker". Available agents: oracle')
|
||||
})
|
||||
|
||||
test("normalizes matched agent model string before returning categoryModel", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["grok-3", "gpt-5.3-codex"] },
|
||||
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" })
|
||||
})
|
||||
|
||||
test("matches agents even when zero-width characters are present in the requested name", async () => {
|
||||
//#given
|
||||
const args = createBaseArgs({ subagent_type: "\uFEFFSisyphus - Ultraworker" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "\u200BSisyphus - Ultraworker", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "oracle", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("Sisyphus - Ultraworker")
|
||||
})
|
||||
|
||||
test("uses agent override fallback_models for subagent runtime fallback chain", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { quotio: ["claude-haiku-4-5"] },
|
||||
connected: ["quotio"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
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" },
|
||||
])
|
||||
})
|
||||
|
||||
test("uses category fallback_models when agent override points at category", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { anthropic: ["claude-haiku-4-5"] },
|
||||
connected: ["anthropic"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
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 },
|
||||
])
|
||||
})
|
||||
|
||||
test("promotes object-style fallback model settings to categoryModel when subagent fallback becomes initial model", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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" },
|
||||
})
|
||||
})
|
||||
|
||||
test("does not apply object-style fallback settings when the subagent primary model matches directly", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4-preview"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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",
|
||||
})
|
||||
})
|
||||
|
||||
test("matches promoted fallback settings after fuzzy model resolution", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4-preview"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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" },
|
||||
})
|
||||
})
|
||||
|
||||
test("prefers exact promoted fallback match over earlier fuzzy prefix match", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4-preview"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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",
|
||||
})
|
||||
})
|
||||
|
||||
test("matches promoted fallback settings when fuzzy resolution extends configured model without hyphen", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4o"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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",
|
||||
})
|
||||
})
|
||||
|
||||
test("does not use unavailable matchedAgent.model as fallback for custom subagent", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { minimaxi: ["MiniMax-M2.7"] },
|
||||
connected: ["minimaxi"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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")
|
||||
})
|
||||
|
||||
test("uses matchedAgent.model as fallback when model is available", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { minimaxi: ["MiniMax-M2.7-highspeed"] },
|
||||
connected: ["minimaxi"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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" })
|
||||
})
|
||||
|
||||
test("prefers the most specific prefix match when fallback entries share a prefix", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-4o-preview"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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",
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves category temperature when fallback entry leaves temperature undefined", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.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: {
|
||||
category: "research",
|
||||
},
|
||||
} as ExecutorContext["agentOverrides"],
|
||||
userCategories: {
|
||||
research: {
|
||||
fallback_models: [
|
||||
{
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "max",
|
||||
},
|
||||
],
|
||||
temperature: 0.55,
|
||||
top_p: 0.45,
|
||||
},
|
||||
} as ExecutorContext["userCategories"],
|
||||
}
|
||||
)
|
||||
|
||||
//#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: "max",
|
||||
temperature: 0.55,
|
||||
top_p: 0.45,
|
||||
})
|
||||
})
|
||||
|
||||
test("applies category tuning params in the cold-cache override path", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: {},
|
||||
connected: [],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.mockReturnValue([])
|
||||
const args = createBaseArgs({ subagent_type: "explore" })
|
||||
const executorCtx = createExecutorContext(
|
||||
async () => ([
|
||||
{ name: "explore", mode: "subagent", model: "openai/gpt-5.4" },
|
||||
]),
|
||||
{
|
||||
agentOverrides: {
|
||||
explore: {
|
||||
category: "research",
|
||||
},
|
||||
} as ExecutorContext["agentOverrides"],
|
||||
userCategories: {
|
||||
research: {
|
||||
model: "openai/gpt-5.4",
|
||||
variant: "high",
|
||||
temperature: 0.61,
|
||||
top_p: 0.62,
|
||||
maxTokens: 3200,
|
||||
reasoningEffort: "medium",
|
||||
thinking: { type: "disabled" },
|
||||
},
|
||||
} as ExecutorContext["userCategories"],
|
||||
}
|
||||
)
|
||||
|
||||
//#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: "high",
|
||||
temperature: 0.61,
|
||||
top_p: 0.62,
|
||||
maxTokens: 3200,
|
||||
reasoningEffort: "medium",
|
||||
thinking: { type: "disabled" },
|
||||
})
|
||||
})
|
||||
|
||||
test("resolves user agent from loadUserAgents when calling task(subagent_type=...)", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.mockReturnValue(["openai"])
|
||||
loadUserAgentsMock.mockImplementation(() => ({
|
||||
"my-user-agent": {
|
||||
description: "A user agent",
|
||||
mode: "subagent",
|
||||
prompt: "Do something",
|
||||
model: "openai/gpt-5.4",
|
||||
},
|
||||
}))
|
||||
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")
|
||||
})
|
||||
|
||||
test("resolves project agent from loadProjectAgents when calling task(subagent_type=...)", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { anthropic: ["claude-sonnet-4"] },
|
||||
connected: ["anthropic"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.mockReturnValue(["anthropic"])
|
||||
loadProjectAgentsMock.mockImplementation(() => ({
|
||||
"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")
|
||||
})
|
||||
|
||||
test("server agent takes precedence over user agent with same name", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { openai: ["gpt-5.4", "gpt-3.5"] },
|
||||
connected: ["openai"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.mockReturnValue(["openai"])
|
||||
loadUserAgentsMock.mockImplementation(() => ({
|
||||
"explore": {
|
||||
description: "User explore agent",
|
||||
mode: "subagent",
|
||||
prompt: "User prompt",
|
||||
model: "openai/gpt-3.5",
|
||||
},
|
||||
}))
|
||||
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")
|
||||
expect(result.categoryModel?.modelID).toBe("gpt-5.4")
|
||||
})
|
||||
|
||||
test("project agent takes precedence over user agent with same name", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: { minimaxi: ["MiniMax-M2.7-highspeed", "claude-3-haiku"] },
|
||||
connected: ["minimaxi"],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
readConnectedProvidersCacheMock.mockReturnValue(["minimaxi"])
|
||||
loadUserAgentsMock.mockImplementation(() => ({
|
||||
"my-custom-agent": {
|
||||
description: "User agent",
|
||||
mode: "subagent",
|
||||
prompt: "User prompt",
|
||||
model: "minimaxi/claude-3-haiku",
|
||||
},
|
||||
}))
|
||||
loadProjectAgentsMock.mockImplementation(() => ({
|
||||
"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")
|
||||
expect(result.categoryModel?.modelID).toBe("MiniMax-M2.7-highspeed")
|
||||
})
|
||||
|
||||
test("filters out primary agents from user/project when resolving", async () => {
|
||||
//#given
|
||||
loadUserAgentsMock.mockImplementation(() => ({
|
||||
"my-primary-agent": {
|
||||
description: "A primary agent",
|
||||
mode: "primary",
|
||||
prompt: "I am primary",
|
||||
},
|
||||
}))
|
||||
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("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveSubagentExecution - agent name sanitization", () => {
|
||||
let resolveSubagentExecution: SubagentResolverModule["resolveSubagentExecution"]
|
||||
|
||||
beforeEach(async () => {
|
||||
mock.restore()
|
||||
logMock.mockClear()
|
||||
readConnectedProvidersCacheMock.mockReset()
|
||||
readProviderModelsCacheMock.mockReset()
|
||||
readConnectedProvidersCacheMock.mockReturnValue(null)
|
||||
readProviderModelsCacheMock.mockReturnValue(null)
|
||||
loadUserAgentsMock.mockReset()
|
||||
loadProjectAgentsMock.mockReset()
|
||||
loadUserAgentsMock.mockImplementation(() => ({}))
|
||||
loadProjectAgentsMock.mockImplementation(() => ({}))
|
||||
mock.module("../../../shared/logger", () => ({
|
||||
log: logMock,
|
||||
}))
|
||||
mock.module("../../../shared/connected-providers-cache", () => ({
|
||||
readConnectedProvidersCache: readConnectedProvidersCacheMock,
|
||||
readProviderModelsCache: readProviderModelsCacheMock,
|
||||
hasConnectedProvidersCache: () => readConnectedProvidersCacheMock() !== null,
|
||||
hasProviderModelsCache: () => readProviderModelsCacheMock() !== null,
|
||||
_resetMemCacheForTesting: () => {},
|
||||
}))
|
||||
mock.module("../../../features/claude-code-agent-loader/loader", () => ({
|
||||
loadUserAgents: loadUserAgentsMock,
|
||||
loadProjectAgents: loadProjectAgentsMock,
|
||||
}))
|
||||
mock.module("../../../features/claude-code-agent-loader", () => ({
|
||||
loadUserAgents: loadUserAgentsMock,
|
||||
loadProjectAgents: loadProjectAgentsMock,
|
||||
}))
|
||||
;({ resolveSubagentExecution } = await importFreshSubagentResolverModule())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
test("strips backslash-wrapped agent names like \\hephaestus\\", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: {},
|
||||
connected: [],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: "\\hephaestus\\" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "Hephaestus - Deep Agent", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("Hephaestus - Deep Agent")
|
||||
})
|
||||
|
||||
test("strips double-quoted agent names", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: {},
|
||||
connected: [],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: '"oracle"' })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "oracle", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("oracle")
|
||||
})
|
||||
|
||||
test("strips single-quoted agent names", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: {},
|
||||
connected: [],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: "'explore'" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "explore", mode: "subagent" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "sisyphus", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("explore")
|
||||
})
|
||||
|
||||
test("matches runtime agent names that include invisible sort prefixes", async () => {
|
||||
//#given
|
||||
readProviderModelsCacheMock.mockReturnValue({
|
||||
models: {},
|
||||
connected: [],
|
||||
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||
})
|
||||
const args = createBaseArgs({ subagent_type: "Sisyphus - Ultraworker" })
|
||||
const executorCtx = createExecutorContext(async () => ([
|
||||
{ name: "\u200BSisyphus - Ultraworker", mode: "subagent", model: "openai/gpt-5.3-codex" },
|
||||
]))
|
||||
|
||||
//#when
|
||||
const result = await resolveSubagentExecution(args, executorCtx, "oracle", "deep")
|
||||
|
||||
//#then
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.agentToUse).toBe("Sisyphus - Ultraworker")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user