Files
oh-my-opencode/src/tools/delegate-task/category-resolver.test.ts
T

420 lines
12 KiB
TypeScript
Raw Normal View History

declare const require: (name: string) => any
const { describe, test, expect, beforeEach, afterEach, spyOn, mock } = require("bun:test")
import { resolveCategoryExecution } from "./category-resolver"
import type { ExecutorContext } from "./executor-types"
import * as connectedProvidersCache from "../../shared/connected-providers-cache"
describe("resolveCategoryExecution", () => {
let connectedProvidersSpy: ReturnType<typeof spyOn> | undefined
let providerModelsSpy: ReturnType<typeof spyOn> | undefined
let hasConnectedProvidersSpy: ReturnType<typeof spyOn> | undefined
let hasProviderModelsSpy: ReturnType<typeof spyOn> | undefined
beforeEach(() => {
mock.restore()
connectedProvidersSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
providerModelsSpy = spyOn(connectedProvidersCache, "readProviderModelsCache").mockReturnValue(null)
hasConnectedProvidersSpy = spyOn(connectedProvidersCache, "hasConnectedProvidersCache").mockReturnValue(false)
hasProviderModelsSpy = spyOn(connectedProvidersCache, "hasProviderModelsCache").mockReturnValue(false)
})
afterEach(() => {
connectedProvidersSpy?.mockRestore()
providerModelsSpy?.mockRestore()
hasConnectedProvidersSpy?.mockRestore()
hasProviderModelsSpy?.mockRestore()
})
const createMockExecutorContext = (): ExecutorContext => ({
client: {} as any,
manager: {} as any,
directory: "/tmp/test",
userCategories: {},
sisyphusJuniorModel: undefined,
})
test("returns unpinned resolution when category cache is not ready on first run", async () => {
//#given
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {},
}
const inheritedModel = undefined
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
//#when
const result = await resolveCategoryExecution(args, executorCtx, inheritedModel, systemDefaultModel)
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBeUndefined()
expect(result.categoryModel).toBeUndefined()
expect(result.agentToUse).toBeDefined()
})
test("returns 'unknown category' error for truly unknown categories", async () => {
//#given
const args = {
category: "definitely-not-a-real-category-xyz123",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
const inheritedModel = undefined
const systemDefaultModel = "anthropic/claude-sonnet-4-6"
//#when
const result = await resolveCategoryExecution(args, executorCtx, inheritedModel, systemDefaultModel)
//#then
expect(result.error).toBeDefined()
expect(result.error).toContain("Unknown category")
expect(result.error).toContain("definitely-not-a-real-category-xyz123")
})
test("uses category fallback_models for background/runtime fallback chain", async () => {
//#given
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
model: "quotio/claude-opus-4-6",
fallback_models: ["quotio/kimi-k2.5", "openai/gpt-5.2(high)"],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.fallbackChain).toEqual([
{ providers: ["quotio"], model: "kimi-k2.5", variant: undefined },
{ providers: ["openai"], model: "gpt-5.2", variant: "high" },
])
})
test("promotes object-style fallback model settings to categoryModel when 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 agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
fallback_models: [
{
model: "openai/gpt-5.4 high",
variant: "low",
reasoningEffort: "high",
temperature: 0.4,
top_p: 0.7,
maxTokens: 4096,
thinking: { type: "disabled" },
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-5.4")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4",
variant: "low",
reasoningEffort: "high",
temperature: 0.4,
top_p: 0.7,
maxTokens: 4096,
thinking: { type: "disabled" },
})
cacheSpy.mockRestore()
agentsSpy.mockRestore()
})
test("does not apply object-style fallback settings when the configured 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 agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
model: "openai/gpt-5.4-preview",
fallback_models: [
{
model: "openai/gpt-5.4",
variant: "low",
reasoningEffort: "high",
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-5.4-preview")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4-preview",
variant: "medium",
})
cacheSpy.mockRestore()
agentsSpy.mockRestore()
})
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 agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
fallback_models: [
{
model: "openai/gpt-5.4",
variant: "low",
reasoningEffort: "high",
temperature: 0.6,
top_p: 0.5,
maxTokens: 1234,
thinking: { type: "disabled" },
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-5.4-preview")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4-preview",
variant: "low",
reasoningEffort: "high",
temperature: 0.6,
top_p: 0.5,
maxTokens: 1234,
thinking: { type: "disabled" },
})
cacheSpy.mockRestore()
agentsSpy.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 agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
fallback_models: [
{
model: "openai/gpt-5.4",
variant: "low",
reasoningEffort: "medium",
},
{
model: "openai/gpt-5.4-preview",
variant: "max",
reasoningEffort: "high",
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-5.4-preview")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4-preview",
variant: "max",
reasoningEffort: "high",
})
cacheSpy.mockRestore()
agentsSpy.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 agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
fallback_models: [
{
model: "openai/gpt-5.4",
variant: "low",
reasoningEffort: "high",
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-5.4o")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-5.4o",
variant: "low",
reasoningEffort: "high",
})
cacheSpy.mockRestore()
agentsSpy.mockRestore()
})
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"] },
connected: ["openai"],
updatedAt: "2026-03-03T00:00:00.000Z",
})
const agentsSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
const args = {
category: "deep",
prompt: "test prompt",
description: "Test task",
run_in_background: false,
load_skills: [],
blockedBy: undefined,
enableSkillTools: false,
}
const executorCtx = createMockExecutorContext()
executorCtx.userCategories = {
deep: {
fallback_models: [
{
model: "openai/gpt-4",
variant: "low",
reasoningEffort: "medium",
},
{
model: "openai/gpt-4o",
variant: "max",
reasoningEffort: "high",
},
],
},
}
//#when
const result = await resolveCategoryExecution(args, executorCtx, undefined, "anthropic/claude-sonnet-4-6")
//#then
expect(result.error).toBeUndefined()
expect(result.actualModel).toBe("openai/gpt-4o")
expect(result.categoryModel).toEqual({
providerID: "openai",
modelID: "gpt-4o",
variant: "max",
reasoningEffort: "high",
})
cacheSpy.mockRestore()
agentsSpy.mockRestore()
})
})