fix(config): stop sisyphus-junior from inheriting UI-selected model

This commit is contained in:
YeonGyu-Kim
2026-02-06 17:44:47 +09:00
parent 3be722b3b1
commit 3a0d7e8dc3
4 changed files with 119 additions and 280 deletions

View File

@@ -1,273 +0,0 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";
import type { PluginInput } from "@opencode-ai/plugin";
let currentConfig: Record<string, unknown> = {};
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<string>(),
}));
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");
});
});

View File

@@ -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<string, unknown> = {
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<string, { model?: string }>
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<string, unknown> = {
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<string, { model?: string }>
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

View File

@@ -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) {

View File

@@ -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")