/// import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test" import { resolveCategoryConfig, createConfigHandler } from "./config-handler" import type { CategoryConfig } from "../config/schema" import type { OhMyOpenCodeConfig } from "../config" import { getAgentDisplayName } from "../shared/agent-display-names" import * as agents from "../agents" import * as sisyphusJunior from "../agents/sisyphus-junior" import * as commandLoader from "../features/claude-code-command-loader" import * as builtinCommands from "../features/builtin-commands" import * as skillLoader from "../features/opencode-skill-loader" import * as agentLoader from "../features/claude-code-agent-loader" import * as mcpLoader from "../features/claude-code-mcp-loader" import * as pluginLoader from "../features/claude-code-plugin-loader" import * as mcpModule from "../mcp" import * as shared from "../shared" import * as configDir from "../shared/opencode-config-dir" import * as permissionCompat from "../shared/permission-compat" import * as modelResolver from "../shared/model-resolver" beforeEach(() => { spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, }) spyOn(commandLoader, "loadUserCommands" as any).mockResolvedValue({}) spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({}) spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({}) spyOn(commandLoader, "loadOpencodeProjectCommands" as any).mockResolvedValue({}) spyOn(builtinCommands, "loadBuiltinCommands" as any).mockReturnValue({}) spyOn(skillLoader, "loadUserSkills" as any).mockResolvedValue({}) spyOn(skillLoader, "loadProjectSkills" as any).mockResolvedValue({}) spyOn(skillLoader, "loadOpencodeGlobalSkills" as any).mockResolvedValue({}) spyOn(skillLoader, "loadOpencodeProjectSkills" as any).mockResolvedValue({}) spyOn(skillLoader, "discoverUserClaudeSkills" as any).mockResolvedValue([]) spyOn(skillLoader, "discoverProjectClaudeSkills" as any).mockResolvedValue([]) spyOn(skillLoader, "discoverOpencodeGlobalSkills" as any).mockResolvedValue([]) spyOn(skillLoader, "discoverOpencodeProjectSkills" as any).mockResolvedValue([]) spyOn(agentLoader, "loadUserAgents" as any).mockReturnValue({}) spyOn(agentLoader, "loadProjectAgents" as any).mockReturnValue({}) spyOn(mcpLoader, "loadMcpConfigs" as any).mockResolvedValue({ servers: {} }) spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({ commands: {}, skills: {}, agents: {}, mcpServers: {}, hooksConfigs: [], plugins: [], errors: [], }) spyOn(mcpModule, "createBuiltinMcps" as any).mockReturnValue({}) spyOn(shared, "log" as any).mockImplementation(() => {}) spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set(["anthropic/claude-opus-4-6"])) spyOn(shared, "readConnectedProvidersCache" as any).mockReturnValue(null) spyOn(configDir, "getOpenCodeConfigPaths" as any).mockReturnValue({ global: "/tmp/.config/opencode", project: "/tmp/.opencode", }) spyOn(permissionCompat, "migrateAgentConfig" as any).mockImplementation((config: Record) => config) spyOn(modelResolver, "resolveModelWithFallback" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6" }) }) afterEach(() => { (agents.createBuiltinAgents as any)?.mockRestore?.() ;(sisyphusJunior.createSisyphusJuniorAgentWithOverrides as any)?.mockRestore?.() ;(commandLoader.loadUserCommands as any)?.mockRestore?.() ;(commandLoader.loadProjectCommands as any)?.mockRestore?.() ;(commandLoader.loadOpencodeGlobalCommands as any)?.mockRestore?.() ;(commandLoader.loadOpencodeProjectCommands as any)?.mockRestore?.() ;(builtinCommands.loadBuiltinCommands as any)?.mockRestore?.() ;(skillLoader.loadUserSkills as any)?.mockRestore?.() ;(skillLoader.loadProjectSkills as any)?.mockRestore?.() ;(skillLoader.loadOpencodeGlobalSkills as any)?.mockRestore?.() ;(skillLoader.loadOpencodeProjectSkills as any)?.mockRestore?.() ;(skillLoader.discoverUserClaudeSkills as any)?.mockRestore?.() ;(skillLoader.discoverProjectClaudeSkills as any)?.mockRestore?.() ;(skillLoader.discoverOpencodeGlobalSkills as any)?.mockRestore?.() ;(skillLoader.discoverOpencodeProjectSkills as any)?.mockRestore?.() ;(agentLoader.loadUserAgents as any)?.mockRestore?.() ;(agentLoader.loadProjectAgents as any)?.mockRestore?.() ;(mcpLoader.loadMcpConfigs as any)?.mockRestore?.() ;(pluginLoader.loadAllPluginComponents as any)?.mockRestore?.() ;(mcpModule.createBuiltinMcps as any)?.mockRestore?.() ;(shared.log as any)?.mockRestore?.() ;(shared.fetchAvailableModels as any)?.mockRestore?.() ;(shared.readConnectedProvidersCache as any)?.mockRestore?.() ;(configDir.getOpenCodeConfigPaths as any)?.mockRestore?.() ;(permissionCompat.migrateAgentConfig as any)?.mockRestore?.() ;(modelResolver.resolveModelWithFallback as any)?.mockRestore?.() }) describe("Sisyphus-Junior model inheritance", () => { test("does not inherit UI-selected model as system default", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "opencode/kimi-k2.5-free", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then const agentConfig = config.agent as Record expect(agentConfig[getAgentDisplayName("sisyphus-junior")]?.model).toBe( sisyphusJunior.SISYPHUS_JUNIOR_DEFAULTS.model ) }) test("uses explicitly configured sisyphus-junior model", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = { agents: { "sisyphus-junior": { model: "openai/gpt-5.3-codex", }, }, } const config: Record = { model: "opencode/kimi-k2.5-free", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then const agentConfig = config.agent as Record expect(agentConfig[getAgentDisplayName("sisyphus-junior")]?.model).toBe( "openai/gpt-5.3-codex" ) }) }) describe("Plan agent demote behavior", () => { test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => { // #given const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { mockResolvedValue: (value: Record) => void } createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, atlas: { name: "atlas", prompt: "test", mode: "primary" }, }) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then const keys = Object.keys(config.agent as Record) const coreAgents = [ getAgentDisplayName("sisyphus"), getAgentDisplayName("hephaestus"), getAgentDisplayName("prometheus"), getAgentDisplayName("atlas"), ] const ordered = keys.filter((key) => coreAgents.includes(key)) expect(ordered).toEqual(coreAgents) }) test("plan agent should be demoted to subagent without inheriting prometheus prompt", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, replace_plan: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", 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 - plan is demoted to subagent but does NOT inherit prometheus prompt const agents = config.agent as Record expect(agents.plan).toBeDefined() expect(agents.plan.mode).toBe("subagent") expect(agents.plan.prompt).toBeUndefined() expect(agents[getAgentDisplayName("prometheus")]?.prompt).toBeDefined() }) test("plan agent remains unchanged when planner is disabled", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: false, }, } const config: Record = { model: "anthropic/claude-opus-4-6", 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 - plan is not touched, prometheus is not created const agents = config.agent as Record expect(agents[getAgentDisplayName("prometheus")]).toBeUndefined() expect(agents.plan).toBeDefined() expect(agents.plan.mode).toBe("primary") expect(agents.plan.prompt).toBe("original plan prompt") }) test("prometheus should have mode 'all' to be callable via task", async () => { // given const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", 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 const prometheusKey = getAgentDisplayName("prometheus") expect(agents[prometheusKey]).toBeDefined() expect(agents[prometheusKey].mode).toBe("all") }) }) describe("Agent permission defaults", () => { test("hephaestus should allow task", async () => { // #given const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { mockResolvedValue: (value: Record) => void } createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, }) const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then const agentConfig = config.agent as Record }> const hephaestusKey = getAgentDisplayName("hephaestus") expect(agentConfig[hephaestusKey]).toBeDefined() expect(agentConfig[hephaestusKey].permission?.task).toBe("allow") }) }) describe("default_agent behavior with Sisyphus orchestration", () => { test("canonicalizes configured default_agent key to display name", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", default_agent: "hephaestus", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) }) test("preserves existing display-name default_agent", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = {} const displayName = getAgentDisplayName("hephaestus") const config: Record = { model: "anthropic/claude-opus-4-6", default_agent: displayName, agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then expect(config.default_agent).toBe(displayName) }) test("sets default_agent to sisyphus when missing", async () => { // #given const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) }) }) describe("Prometheus category config resolution", () => { test("resolves ultrabrain category config", () => { // given const categoryName = "ultrabrain" // when const config = resolveCategoryConfig(categoryName) // then expect(config).toBeDefined() expect(config?.model).toBe("openai/gpt-5.3-codex") expect(config?.variant).toBe("xhigh") }) test("resolves visual-engineering category config", () => { // given const categoryName = "visual-engineering" // when const config = resolveCategoryConfig(categoryName) // then expect(config).toBeDefined() expect(config?.model).toBe("google/gemini-3-pro") }) test("user categories override default categories", () => { // given const categoryName = "ultrabrain" const userCategories: Record = { 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 = { "visual-engineering": { model: "custom/visual-model", }, } // when const config = resolveCategoryConfig(categoryName, userCategories) // then - falls back to DEFAULT_CATEGORIES expect(config).toBeDefined() expect(config?.model).toBe("openai/gpt-5.3-codex") expect(config?.variant).toBe("xhigh") }) test("preserves all category properties (temperature, top_p, tools, etc.)", () => { // given const categoryName = "custom-category" const userCategories: Record = { "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 }) }) }) describe("Prometheus direct override priority over category", () => { test("direct reasoningEffort takes priority over category reasoningEffort", async () => { // given - category has reasoningEffort=xhigh, direct override says "low" const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, categories: { "test-planning": { model: "openai/gpt-5.2", reasoningEffort: "xhigh", }, }, agents: { prometheus: { category: "test-planning", reasoningEffort: "low", }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // when await handler(config) // then - direct override's reasoningEffort wins const agents = config.agent as Record const pKey = getAgentDisplayName("prometheus") expect(agents[pKey]).toBeDefined() expect(agents[pKey].reasoningEffort).toBe("low") }) test("category reasoningEffort applied when no direct override", async () => { // given - category has reasoningEffort but no direct override const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, categories: { "reasoning-cat": { model: "openai/gpt-5.2", reasoningEffort: "high", }, }, agents: { prometheus: { category: "reasoning-cat", }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // when await handler(config) // then - category's reasoningEffort is applied const agents = config.agent as Record const pKey = getAgentDisplayName("prometheus") expect(agents[pKey]).toBeDefined() expect(agents[pKey].reasoningEffort).toBe("high") }) test("direct temperature takes priority over category temperature", async () => { // given const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, categories: { "temp-cat": { model: "openai/gpt-5.2", temperature: 0.8, }, }, agents: { prometheus: { category: "temp-cat", temperature: 0.1, }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // when await handler(config) // then - direct temperature wins over category const agents = config.agent as Record const pKey = getAgentDisplayName("prometheus") expect(agents[pKey]).toBeDefined() expect(agents[pKey].temperature).toBe(0.1) }) test("prometheus prompt_append is appended to base prompt", async () => { // #given - prometheus override with prompt_append const customInstructions = "## Custom Project Rules\nUse max 2 commits." const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, agents: { prometheus: { prompt_append: customInstructions, }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // #when await handler(config) // #then - prompt_append is appended to base prompt, not overwriting it const agents = config.agent as Record const pKey = getAgentDisplayName("prometheus") expect(agents[pKey]).toBeDefined() expect(agents[pKey].prompt).toContain("Prometheus") expect(agents[pKey].prompt).toContain(customInstructions) expect(agents[pKey].prompt!.endsWith(customInstructions)).toBe(true) }) }) describe("Plan agent model inheritance from prometheus", () => { test("plan agent inherits all model-related settings from resolved prometheus config", async () => { //#given - prometheus resolves to claude-opus-4-6 with model settings spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6", provenance: "provider-fallback", variant: "max", }) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, replace_plan: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", 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 - plan inherits model and variant from prometheus, but NOT prompt const agents = config.agent as Record expect(agents.plan).toBeDefined() expect(agents.plan.mode).toBe("subagent") expect(agents.plan.model).toBe("anthropic/claude-opus-4-6") expect(agents.plan.variant).toBe("max") expect(agents.plan.prompt).toBeUndefined() }) test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => { //#given - prometheus configured with category that has temperature and reasoningEffort spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ model: "openai/gpt-5.2", provenance: "override", variant: "high", }) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, replace_plan: true, }, agents: { prometheus: { model: "openai/gpt-5.2", variant: "high", temperature: 0.3, top_p: 0.9, maxTokens: 16000, reasoningEffort: "high", textVerbosity: "medium", thinking: { type: "enabled", budgetTokens: 8000 }, }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then - plan inherits ALL model-related settings from resolved prometheus const agents = config.agent as Record> expect(agents.plan).toBeDefined() expect(agents.plan.mode).toBe("subagent") expect(agents.plan.model).toBe("openai/gpt-5.2") expect(agents.plan.variant).toBe("high") expect(agents.plan.temperature).toBe(0.3) expect(agents.plan.top_p).toBe(0.9) expect(agents.plan.maxTokens).toBe(16000) expect(agents.plan.reasoningEffort).toBe("high") expect(agents.plan.textVerbosity).toBe("medium") expect(agents.plan.thinking).toEqual({ type: "enabled", budgetTokens: 8000 }) }) test("plan agent user override takes priority over prometheus inherited settings", async () => { //#given - prometheus resolves to opus, but user has plan override for gpt-5.2 spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6", provenance: "provider-fallback", variant: "max", }) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, replace_plan: true, }, agents: { plan: { model: "openai/gpt-5.2", variant: "high", temperature: 0.5, }, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then - plan uses its own override, not prometheus settings const agents = config.agent as Record> expect(agents.plan.model).toBe("openai/gpt-5.2") expect(agents.plan.variant).toBe("high") expect(agents.plan.temperature).toBe(0.5) }) test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => { //#given spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({ model: "anthropic/claude-opus-4-6", provenance: "provider-fallback", variant: "max", }) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, replace_plan: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then - plan has model settings but NOT prompt/description/color const agents = config.agent as Record> expect(agents.plan.model).toBe("anthropic/claude-opus-4-6") expect(agents.plan.prompt).toBeUndefined() expect(agents.plan.description).toBeUndefined() expect(agents.plan.color).toBeUndefined() }) }) describe("Deadlock prevention - fetchAvailableModels must not receive client", () => { test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => { // given - This test ensures we don't regress on issue #1301 // Passing client to fetchAvailableModels during config handler causes deadlock: // - Plugin init waits for server response (client.provider.list()) // - Server waits for plugin init to complete before handling requests const fetchSpy = spyOn(shared, "fetchAvailableModels" as any).mockResolvedValue(new Set()) const pluginConfig: OhMyOpenCodeConfig = { sisyphus_agent: { planner_enabled: true, }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const mockClient = { provider: { list: () => Promise.resolve({ data: { connected: [] } }) }, model: { list: () => Promise.resolve({ data: [] }) }, } const handler = createConfigHandler({ ctx: { directory: "/tmp", client: mockClient }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) // when await handler(config) // then - fetchAvailableModels must be called with undefined as first argument (no client) // This prevents the deadlock described in issue #1301 expect(fetchSpy).toHaveBeenCalled() const firstCallArgs = fetchSpy.mock.calls[0] expect(firstCallArgs[0]).toBeUndefined() fetchSpy.mockRestore?.() }) }) describe("config-handler plugin loading error boundary (#1559)", () => { test("returns empty defaults when loadAllPluginComponents throws", async () => { //#given ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash")) const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then expect(config.agent).toBeDefined() }) test("returns empty defaults when loadAllPluginComponents times out", async () => { //#given ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() spyOn(pluginLoader, "loadAllPluginComponents" as any).mockImplementation( () => new Promise(() => {}) ) const pluginConfig: OhMyOpenCodeConfig = { experimental: { plugin_load_timeout_ms: 100 }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then expect(config.agent).toBeDefined() }, 5000) test("logs error when loadAllPluginComponents fails", async () => { //#given ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() spyOn(pluginLoader, "loadAllPluginComponents" as any).mockRejectedValue(new Error("crash")) const logSpy = shared.log as ReturnType const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then const logCalls = logSpy.mock.calls.map((c: unknown[]) => c[0]) const hasPluginFailureLog = logCalls.some( (msg: string) => typeof msg === "string" && msg.includes("Plugin loading failed") ) expect(hasPluginFailureLog).toBe(true) }) test("passes through plugin data on successful load (identity test)", async () => { //#given ;(pluginLoader.loadAllPluginComponents as any).mockRestore?.() spyOn(pluginLoader, "loadAllPluginComponents" as any).mockResolvedValue({ commands: { "test-cmd": { description: "test", template: "test" } }, skills: {}, agents: {}, mcpServers: {}, hooksConfigs: [], plugins: [{ name: "test-plugin", version: "1.0.0" }], errors: [], }) const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then const commands = config.command as Record expect(commands["test-cmd"]).toBeDefined() }) }) describe("per-agent todowrite/todoread deny when task_system enabled", () => { const PRIMARY_AGENTS = [ getAgentDisplayName("sisyphus"), getAgentDisplayName("hephaestus"), getAgentDisplayName("atlas"), getAgentDisplayName("prometheus"), getAgentDisplayName("sisyphus-junior"), ] test("denies todowrite and todoread for primary agents when task_system is enabled", async () => { //#given const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { mockResolvedValue: (value: Record) => void } createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, atlas: { name: "atlas", prompt: "test", mode: "primary" }, prometheus: { name: "prometheus", prompt: "test", mode: "primary" }, "sisyphus-junior": { name: "sisyphus-junior", prompt: "test", mode: "subagent" }, oracle: { name: "oracle", prompt: "test", mode: "subagent" }, }) const pluginConfig: OhMyOpenCodeConfig = { experimental: { task_system: true }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then const agentResult = config.agent as Record }> for (const agentName of PRIMARY_AGENTS) { expect(agentResult[agentName]?.permission?.todowrite).toBe("deny") expect(agentResult[agentName]?.permission?.todoread).toBe("deny") } }) test("does not deny todowrite/todoread when task_system is disabled", async () => { //#given const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { mockResolvedValue: (value: Record) => void } createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, }) const pluginConfig: OhMyOpenCodeConfig = { experimental: { task_system: false }, } const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then const agentResult = config.agent as Record }> expect(agentResult[getAgentDisplayName("sisyphus")]?.permission?.todowrite).toBeUndefined() expect(agentResult[getAgentDisplayName("sisyphus")]?.permission?.todoread).toBeUndefined() expect(agentResult[getAgentDisplayName("hephaestus")]?.permission?.todowrite).toBeUndefined() expect(agentResult[getAgentDisplayName("hephaestus")]?.permission?.todoread).toBeUndefined() }) test("does not deny todowrite/todoread when task_system is undefined", async () => { //#given const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as { mockResolvedValue: (value: Record) => void } createBuiltinAgentsMock.mockResolvedValue({ sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, }) const pluginConfig: OhMyOpenCodeConfig = {} const config: Record = { model: "anthropic/claude-opus-4-6", agent: {}, } const handler = createConfigHandler({ ctx: { directory: "/tmp" }, pluginConfig, modelCacheState: { anthropicContext1MEnabled: false, modelContextLimitsCache: new Map(), }, }) //#when await handler(config) //#then const agentResult = config.agent as Record }> expect(agentResult[getAgentDisplayName("sisyphus")]?.permission?.todowrite).toBeUndefined() expect(agentResult[getAgentDisplayName("sisyphus")]?.permission?.todoread).toBeUndefined() }) })