From 3a0d7e8dc31cedd622df8592bd8c8e650975b1c1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 6 Feb 2026 17:44:47 +0900 Subject: [PATCH] fix(config): stop sisyphus-junior from inheriting UI-selected model --- src/index.disabled-tools.test.ts | 273 --------------------- src/plugin-handlers/config-handler.test.ts | 66 ++++- src/plugin-handlers/config-handler.ts | 2 +- src/tools/delegate-task/tools.test.ts | 58 +++++ 4 files changed, 119 insertions(+), 280 deletions(-) delete mode 100644 src/index.disabled-tools.test.ts diff --git a/src/index.disabled-tools.test.ts b/src/index.disabled-tools.test.ts deleted file mode 100644 index f370746f4..000000000 --- a/src/index.disabled-tools.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { beforeEach, describe, expect, mock, test } from "bun:test"; -import type { PluginInput } from "@opencode-ai/plugin"; - -let currentConfig: Record = {}; - -const DUMMY_TOOL = { - description: "dummy", - args: {}, - execute: async () => "ok", -}; - -mock.module("./plugin-config", () => ({ - loadPluginConfig: () => currentConfig, -})); - -mock.module("./shared", () => ({ - log: () => {}, - detectExternalNotificationPlugin: () => ({ - detected: false, - pluginName: null, - allPlugins: [], - }), - getNotificationConflictWarning: () => "", - resetMessageCursor: () => {}, - hasConnectedProvidersCache: () => true, - getOpenCodeVersion: () => null, - isOpenCodeVersionAtLeast: () => false, - OPENCODE_NATIVE_AGENTS_INJECTION_VERSION: "0.0.0", - injectServerAuthIntoClient: () => {}, -})); - -mock.module("./hooks", () => { - const noopHook = { - event: async () => {}, - handler: async () => {}, - "chat.message": async () => {}, - "tool.execute.before": async () => {}, - "tool.execute.after": async () => {}, - "experimental.chat.messages.transform": async () => {}, - }; - - return { - createTodoContinuationEnforcer: () => null, - createContextWindowMonitorHook: () => null, - createSessionRecoveryHook: () => null, - createSessionNotification: () => async () => {}, - createCommentCheckerHooks: () => null, - createToolOutputTruncatorHook: () => null, - createDirectoryAgentsInjectorHook: () => null, - createDirectoryReadmeInjectorHook: () => null, - createEmptyTaskResponseDetectorHook: () => null, - createThinkModeHook: () => null, - createClaudeCodeHooksHook: () => noopHook, - createAnthropicContextWindowLimitRecoveryHook: () => null, - createRulesInjectorHook: () => null, - createBackgroundNotificationHook: () => null, - createAutoUpdateCheckerHook: () => ({ event: async () => {} }), - createKeywordDetectorHook: () => null, - createAgentUsageReminderHook: () => null, - createNonInteractiveEnvHook: () => null, - createInteractiveBashSessionHook: () => null, - createThinkingBlockValidatorHook: () => null, - createCategorySkillReminderHook: () => null, - createRalphLoopHook: () => null, - createAutoSlashCommandHook: () => null, - createEditErrorRecoveryHook: () => null, - createDelegateTaskRetryHook: () => null, - createTaskResumeInfoHook: () => ({ - "tool.execute.after": async () => {}, - }), - createStartWorkHook: () => null, - createAtlasHook: () => null, - createPrometheusMdOnlyHook: () => null, - createSisyphusJuniorNotepadHook: () => null, - createQuestionLabelTruncatorHook: () => null, - createSubagentQuestionBlockerHook: () => null, - createStopContinuationGuardHook: () => null, - createCompactionContextInjector: () => null, - createUnstableAgentBabysitterHook: () => null, - createPreemptiveCompactionHook: () => null, - createTasksTodowriteDisablerHook: () => null, - createWriteExistingFileGuardHook: () => null, - }; -}); - -mock.module("./features/context-injector", () => ({ - contextCollector: {}, - createContextInjectorMessagesTransformHook: () => null, -})); - -mock.module("./shared/agent-variant", () => ({ - applyAgentVariant: () => {}, - resolveAgentVariant: () => undefined, - resolveVariantForModel: () => undefined, -})); - -mock.module("./shared/first-message-variant", () => ({ - createFirstMessageVariantGate: () => ({ - shouldOverride: () => false, - markApplied: () => {}, - markSessionCreated: () => {}, - clear: () => {}, - }), -})); - -mock.module("./features/opencode-skill-loader", () => ({ - discoverUserClaudeSkills: async () => [], - discoverProjectClaudeSkills: async () => [], - discoverOpencodeGlobalSkills: async () => [], - discoverOpencodeProjectSkills: async () => [], - mergeSkills: (...skills: unknown[][]) => skills.flat(), -})); - -mock.module("./features/builtin-skills", () => ({ - createBuiltinSkills: () => [], -})); - -mock.module("./features/claude-code-mcp-loader", () => ({ - getSystemMcpServerNames: () => new Set(), -})); - -mock.module("./features/claude-code-session-state", () => ({ - setMainSession: () => {}, - getMainSessionID: () => undefined, - setSessionAgent: () => {}, - updateSessionAgent: () => {}, - clearSessionAgent: () => {}, -})); - -mock.module("./features/background-agent", () => ({ - BackgroundManager: class BackgroundManager { - constructor(..._args: unknown[]) {} - }, -})); - -mock.module("./features/skill-mcp-manager", () => ({ - SkillMcpManager: class SkillMcpManager { - disconnectSession = async () => {}; - }, -})); - -mock.module("./features/task-toast-manager", () => ({ - initTaskToastManager: () => {}, -})); - -mock.module("./features/tmux-subagent", () => ({ - TmuxSessionManager: class TmuxSessionManager { - constructor(..._args: unknown[]) {} - cleanup = async () => {}; - onSessionCreated = async () => {}; - onSessionDeleted = async () => {}; - }, -})); - -mock.module("./features/boulder-state", () => ({ - clearBoulderState: () => {}, -})); - -mock.module("./plugin-state", () => ({ - createModelCacheState: () => ({}), -})); - -mock.module("./plugin-handlers", () => ({ - createConfigHandler: () => ({}), -})); - -mock.module("./tools", () => ({ - builtinTools: { - foo: DUMMY_TOOL, - bar: DUMMY_TOOL, - }, - createCallOmoAgent: () => DUMMY_TOOL, - createBackgroundTools: () => ({ - background_output: DUMMY_TOOL, - }), - createLookAt: () => DUMMY_TOOL, - createSkillTool: () => DUMMY_TOOL, - createSkillMcpTool: () => DUMMY_TOOL, - createSlashcommandTool: () => DUMMY_TOOL, - discoverCommandsSync: () => [], - sessionExists: () => false, - createDelegateTask: () => DUMMY_TOOL, - interactive_bash: DUMMY_TOOL, - startTmuxCheck: () => {}, - lspManager: { - cleanupTempDirectoryClients: async () => {}, - }, - createTaskCreateTool: () => DUMMY_TOOL, - createTaskGetTool: () => DUMMY_TOOL, - createTaskList: () => DUMMY_TOOL, - createTaskUpdateTool: () => DUMMY_TOOL, -})); - -const { default: OhMyOpenCodePlugin } = await import("./index"); - -describe("disabled_tools config", () => { - beforeEach(() => { - currentConfig = { - experimental: { task_system: false }, - }; - }); - - test("returns all tools when disabled_tools is unset", async () => { - //#given - const ctx = { - directory: "/tmp/omo-test", - client: {}, - } as PluginInput; - - //#when - const plugin = await OhMyOpenCodePlugin(ctx); - const toolNames = Object.keys(plugin.tool).sort(); - - //#then - expect(toolNames).toEqual( - [ - "background_output", - "bar", - "call_omo_agent", - "delegate_task", - "foo", - "interactive_bash", - "look_at", - "skill", - "skill_mcp", - "slashcommand", - ].sort(), - ); - }); - - test("filters out tools listed in disabled_tools", async () => { - //#given - currentConfig = { - experimental: { task_system: false }, - disabled_tools: ["call_omo_agent", "delegate_task"], - }; - - const ctx = { - directory: "/tmp/omo-test", - client: {}, - } as PluginInput; - - //#when - const plugin = await OhMyOpenCodePlugin(ctx); - const toolNames = Object.keys(plugin.tool); - - //#then - expect(toolNames).not.toContain("call_omo_agent"); - expect(toolNames).not.toContain("delegate_task"); - expect(toolNames).toContain("foo"); - expect(toolNames).toContain("background_output"); - }); - - test("matches tool names exactly", async () => { - //#given - currentConfig = { - experimental: { task_system: false }, - disabled_tools: ["call"], - }; - - const ctx = { - directory: "/tmp/omo-test", - client: {}, - } as PluginInput; - - //#when - const plugin = await OhMyOpenCodePlugin(ctx); - const toolNames = Object.keys(plugin.tool); - - //#then - expect(toolNames).toContain("call_omo_agent"); - }); -}); diff --git a/src/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index 2580fe5e3..c53f11da7 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -23,12 +23,6 @@ beforeEach(() => { oracle: { name: "oracle", prompt: "test", mode: "subagent" }, }) - spyOn(sisyphusJunior, "createSisyphusJuniorAgentWithOverrides" as any).mockReturnValue({ - name: "sisyphus-junior", - prompt: "test", - mode: "subagent", - }) - spyOn(commandLoader, "loadUserCommands" as any).mockResolvedValue({}) spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({}) spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({}) @@ -105,6 +99,66 @@ afterEach(() => { ;(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["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["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 diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index e87644fb6..f5c41bb28 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -222,7 +222,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides( pluginConfig.agents?.["sisyphus-junior"], - config.model as string | undefined + undefined ); if (builderEnabled) { diff --git a/src/tools/delegate-task/tools.test.ts b/src/tools/delegate-task/tools.test.ts index effc0bf40..5e878bcef 100644 --- a/src/tools/delegate-task/tools.test.ts +++ b/src/tools/delegate-task/tools.test.ts @@ -1712,6 +1712,64 @@ describe("sisyphus-task", () => { expect(launchInput.model.modelID).toBe("claude-haiku-4-5") }) + test("category delegation ignores UI-selected (Kimi) system default model", async () => { + // given - OpenCode system default model is Kimi (selected from UI) + const { createDelegateTask } = require("./tools") + let launchInput: any + + const mockManager = { + launch: async (input: any) => { + launchInput = input + return { + id: "task-ui-model", + sessionID: "ses_ui_model_test", + description: "UI model inheritance test", + agent: "sisyphus-junior", + status: "running", + } + }, + } + + const mockClient = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: { model: "opencode/kimi-k2.5-free" } }) }, + model: { list: async () => [] }, + session: { + create: async () => ({ data: { id: "test-session" } }), + prompt: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const tool = createDelegateTask({ + manager: mockManager, + client: mockClient, + }) + + const toolContext = { + sessionID: "parent-session", + messageID: "parent-message", + agent: "sisyphus", + abort: new AbortController().signal, + } + + // when - using "quick" category which should use "anthropic/claude-haiku-4-5" + await tool.execute( + { + description: "UI model inheritance test", + prompt: "Do something quick", + category: "quick", + run_in_background: true, + load_skills: [], + }, + toolContext + ) + + // then - category model must win (not Kimi) + expect(launchInput.model.providerID).toBe("anthropic") + expect(launchInput.model.modelID).toBe("claude-haiku-4-5") + }) + test("sisyphus-junior model override takes precedence over category model", async () => { // given - sisyphus-junior override model differs from category default const { createDelegateTask } = require("./tools")