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 connected: string[] updatedAt: string } | null, ) async function importFreshSubagentResolverModule(): Promise { return await import(`../subagent-resolver?test=${Date.now()}-${Math.random()}`) } function createBaseArgs(overrides?: Partial): DelegateTaskArgs { return { description: "Run review", prompt: "Review the current changes", run_in_background: false, load_skills: [], subagent_type: "oracle", ...overrides, } } function createExecutorContext( agentsFn: () => Promise, overrides?: Partial, ): 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) mock.module("../../../shared/logger", () => ({ log: logMock, })) mock.module("../../../shared/connected-providers-cache", () => ({ readConnectedProvidersCache: readConnectedProvidersCacheMock, readProviderModelsCache: readProviderModelsCacheMock, hasConnectedProvidersCache: () => readConnectedProvidersCacheMock() !== null, hasProviderModelsCache: () => readProviderModelsCacheMock() !== null, _resetMemCacheForTesting: () => {}, })) ;({ 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" }, }) }) }) 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) mock.module("../../../shared/logger", () => ({ log: logMock, })) mock.module("../../../shared/connected-providers-cache", () => ({ readConnectedProvidersCache: readConnectedProvidersCacheMock, readProviderModelsCache: readProviderModelsCacheMock, hasConnectedProvidersCache: () => readConnectedProvidersCacheMock() !== null, hasProviderModelsCache: () => readProviderModelsCacheMock() !== null, _resetMemCacheForTesting: () => {}, })) ;({ 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") }) })