2026-01-27 16:06:03 +09:00
|
|
|
import { describe, test, expect, mock, beforeEach } from "bun:test"
|
|
|
|
|
import { resolveCategoryConfig, createConfigHandler } from "./config-handler"
|
2026-01-12 14:04:55 +11:00
|
|
|
import type { CategoryConfig } from "../config/schema"
|
2026-01-27 16:06:03 +09:00
|
|
|
import type { OhMyOpenCodeConfig } from "../config"
|
|
|
|
|
|
|
|
|
|
mock.module("../agents", () => ({
|
|
|
|
|
createBuiltinAgents: async () => ({
|
|
|
|
|
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
|
|
|
|
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../agents/sisyphus-junior", () => ({
|
|
|
|
|
createSisyphusJuniorAgentWithOverrides: () => ({
|
|
|
|
|
name: "sisyphus-junior",
|
|
|
|
|
prompt: "test",
|
|
|
|
|
mode: "subagent",
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/claude-code-command-loader", () => ({
|
|
|
|
|
loadUserCommands: async () => ({}),
|
|
|
|
|
loadProjectCommands: async () => ({}),
|
|
|
|
|
loadOpencodeGlobalCommands: async () => ({}),
|
|
|
|
|
loadOpencodeProjectCommands: async () => ({}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/builtin-commands", () => ({
|
|
|
|
|
loadBuiltinCommands: () => ({}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/opencode-skill-loader", () => ({
|
|
|
|
|
loadUserSkills: async () => ({}),
|
|
|
|
|
loadProjectSkills: async () => ({}),
|
|
|
|
|
loadOpencodeGlobalSkills: async () => ({}),
|
|
|
|
|
loadOpencodeProjectSkills: async () => ({}),
|
|
|
|
|
discoverUserClaudeSkills: async () => [],
|
|
|
|
|
discoverProjectClaudeSkills: async () => [],
|
|
|
|
|
discoverOpencodeGlobalSkills: async () => [],
|
|
|
|
|
discoverOpencodeProjectSkills: async () => [],
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/claude-code-agent-loader", () => ({
|
|
|
|
|
loadUserAgents: () => ({}),
|
|
|
|
|
loadProjectAgents: () => ({}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/claude-code-mcp-loader", () => ({
|
|
|
|
|
loadMcpConfigs: async () => ({ servers: {} }),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../features/claude-code-plugin-loader", () => ({
|
|
|
|
|
loadAllPluginComponents: async () => ({
|
|
|
|
|
commands: {},
|
|
|
|
|
skills: {},
|
|
|
|
|
agents: {},
|
|
|
|
|
mcpServers: {},
|
|
|
|
|
hooksConfigs: [],
|
|
|
|
|
plugins: [],
|
|
|
|
|
errors: [],
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../mcp", () => ({
|
|
|
|
|
createBuiltinMcps: () => ({}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared", () => ({
|
|
|
|
|
log: () => {},
|
|
|
|
|
fetchAvailableModels: async () => new Set(["anthropic/claude-opus-4-5"]),
|
|
|
|
|
readConnectedProvidersCache: () => null,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared/opencode-config-dir", () => ({
|
|
|
|
|
getOpenCodeConfigPaths: () => ({
|
|
|
|
|
global: "/tmp/.config/opencode",
|
|
|
|
|
project: "/tmp/.opencode",
|
|
|
|
|
}),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared/permission-compat", () => ({
|
|
|
|
|
migrateAgentConfig: (config: Record<string, unknown>) => config,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared/migration", () => ({
|
|
|
|
|
AGENT_NAME_MAP: {},
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared/model-resolver", () => ({
|
|
|
|
|
resolveModelWithFallback: () => ({ model: "anthropic/claude-opus-4-5" }),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
mock.module("../shared/model-requirements", () => ({
|
|
|
|
|
AGENT_MODEL_REQUIREMENTS: {
|
2026-01-28 00:54:20 +09:00
|
|
|
sisyphus: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" }] },
|
|
|
|
|
oracle: { fallbackChain: [{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }] },
|
|
|
|
|
librarian: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }] },
|
|
|
|
|
explore: { fallbackChain: [{ providers: ["anthropic", "opencode"], model: "claude-haiku-4-5" }] },
|
|
|
|
|
"multimodal-looker": { fallbackChain: [{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }] },
|
|
|
|
|
prometheus: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" }] },
|
|
|
|
|
metis: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" }] },
|
|
|
|
|
momus: { fallbackChain: [{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" }] },
|
|
|
|
|
atlas: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }] },
|
|
|
|
|
},
|
|
|
|
|
CATEGORY_MODEL_REQUIREMENTS: {
|
|
|
|
|
"visual-engineering": { fallbackChain: [{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }] },
|
|
|
|
|
ultrabrain: { fallbackChain: [{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex" }] },
|
|
|
|
|
artistry: { fallbackChain: [{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" }] },
|
|
|
|
|
quick: { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" }] },
|
|
|
|
|
"unspecified-low": { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" }] },
|
|
|
|
|
"unspecified-high": { fallbackChain: [{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-5" }] },
|
|
|
|
|
writing: { fallbackChain: [{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" }] },
|
2026-01-27 16:06:03 +09:00
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
describe("Plan agent demote behavior", () => {
|
|
|
|
|
test("plan agent should be demoted to subagent mode when replacePlan is true", async () => {
|
|
|
|
|
// #given
|
|
|
|
|
const pluginConfig: OhMyOpenCodeConfig = {
|
|
|
|
|
sisyphus_agent: {
|
|
|
|
|
planner_enabled: true,
|
|
|
|
|
replace_plan: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
const config: Record<string, unknown> = {
|
|
|
|
|
model: "anthropic/claude-opus-4-5",
|
|
|
|
|
agent: {
|
|
|
|
|
plan: {
|
|
|
|
|
name: "plan",
|
|
|
|
|
mode: "primary",
|
|
|
|
|
prompt: "original plan prompt",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
const handler = createConfigHandler({
|
|
|
|
|
ctx: { directory: "/tmp" },
|
|
|
|
|
pluginConfig,
|
|
|
|
|
modelCacheState: {
|
|
|
|
|
anthropicContext1MEnabled: false,
|
|
|
|
|
modelContextLimitsCache: new Map(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
await handler(config)
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
const agents = config.agent as Record<string, { mode?: string; name?: string }>
|
|
|
|
|
expect(agents.plan).toBeDefined()
|
|
|
|
|
expect(agents.plan.mode).toBe("subagent")
|
|
|
|
|
expect(agents.plan.name).toBe("plan")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("prometheus should have mode 'all' to be callable via delegate_task", async () => {
|
|
|
|
|
// #given
|
|
|
|
|
const pluginConfig: OhMyOpenCodeConfig = {
|
|
|
|
|
sisyphus_agent: {
|
|
|
|
|
planner_enabled: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
const config: Record<string, unknown> = {
|
|
|
|
|
model: "anthropic/claude-opus-4-5",
|
|
|
|
|
agent: {},
|
|
|
|
|
}
|
|
|
|
|
const handler = createConfigHandler({
|
|
|
|
|
ctx: { directory: "/tmp" },
|
|
|
|
|
pluginConfig,
|
|
|
|
|
modelCacheState: {
|
|
|
|
|
anthropicContext1MEnabled: false,
|
|
|
|
|
modelContextLimitsCache: new Map(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
await handler(config)
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
const agents = config.agent as Record<string, { mode?: string }>
|
|
|
|
|
expect(agents.prometheus).toBeDefined()
|
|
|
|
|
expect(agents.prometheus.mode).toBe("all")
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-01-12 14:04:55 +11:00
|
|
|
|
|
|
|
|
describe("Prometheus category config resolution", () => {
|
|
|
|
|
test("resolves ultrabrain category config", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "ultrabrain"
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName)
|
|
|
|
|
|
2026-01-20 17:05:17 +09:00
|
|
|
// #then
|
2026-01-12 14:04:55 +11:00
|
|
|
expect(config).toBeDefined()
|
2026-01-20 17:05:17 +09:00
|
|
|
expect(config?.model).toBe("openai/gpt-5.2-codex")
|
|
|
|
|
expect(config?.variant).toBe("xhigh")
|
2026-01-12 14:04:55 +11:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("resolves visual-engineering category config", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "visual-engineering"
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName)
|
|
|
|
|
|
2026-01-20 17:05:17 +09:00
|
|
|
// #then
|
2026-01-12 14:04:55 +11:00
|
|
|
expect(config).toBeDefined()
|
2026-01-24 15:30:35 +09:00
|
|
|
expect(config?.model).toBe("google/gemini-3-pro")
|
2026-01-12 14:04:55 +11:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("user categories override default categories", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "ultrabrain"
|
|
|
|
|
const userCategories: Record<string, CategoryConfig> = {
|
|
|
|
|
ultrabrain: {
|
|
|
|
|
model: "google/antigravity-claude-opus-4-5-thinking",
|
|
|
|
|
temperature: 0.1,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName, userCategories)
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
expect(config).toBeDefined()
|
|
|
|
|
expect(config?.model).toBe("google/antigravity-claude-opus-4-5-thinking")
|
|
|
|
|
expect(config?.temperature).toBe(0.1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("returns undefined for unknown category", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "nonexistent-category"
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName)
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
expect(config).toBeUndefined()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("falls back to default when user category has no entry", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "ultrabrain"
|
|
|
|
|
const userCategories: Record<string, CategoryConfig> = {
|
|
|
|
|
"visual-engineering": {
|
|
|
|
|
model: "custom/visual-model",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName, userCategories)
|
|
|
|
|
|
2026-01-20 17:05:17 +09:00
|
|
|
// #then - falls back to DEFAULT_CATEGORIES
|
2026-01-12 14:04:55 +11:00
|
|
|
expect(config).toBeDefined()
|
2026-01-20 17:05:17 +09:00
|
|
|
expect(config?.model).toBe("openai/gpt-5.2-codex")
|
|
|
|
|
expect(config?.variant).toBe("xhigh")
|
2026-01-12 14:04:55 +11:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("preserves all category properties (temperature, top_p, tools, etc.)", () => {
|
|
|
|
|
// #given
|
|
|
|
|
const categoryName = "custom-category"
|
|
|
|
|
const userCategories: Record<string, CategoryConfig> = {
|
|
|
|
|
"custom-category": {
|
|
|
|
|
model: "test/model",
|
|
|
|
|
temperature: 0.5,
|
|
|
|
|
top_p: 0.9,
|
|
|
|
|
maxTokens: 32000,
|
|
|
|
|
tools: { tool1: true, tool2: false },
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const config = resolveCategoryConfig(categoryName, userCategories)
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
expect(config).toBeDefined()
|
|
|
|
|
expect(config?.model).toBe("test/model")
|
|
|
|
|
expect(config?.temperature).toBe(0.5)
|
|
|
|
|
expect(config?.top_p).toBe(0.9)
|
|
|
|
|
expect(config?.maxTokens).toBe(32000)
|
|
|
|
|
expect(config?.tools).toEqual({ tool1: true, tool2: false })
|
|
|
|
|
})
|
|
|
|
|
})
|