fix(config): stop sisyphus-junior from inheriting UI-selected model
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user