fix(config): stop sisyphus-junior from inheriting UI-selected model
This commit is contained in:
parent
3be722b3b1
commit
3a0d7e8dc3
@ -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" },
|
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, "loadUserCommands" as any).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, "loadProjectCommands" as any).mockResolvedValue({})
|
||||||
spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({})
|
spyOn(commandLoader, "loadOpencodeGlobalCommands" as any).mockResolvedValue({})
|
||||||
@ -105,6 +99,66 @@ afterEach(() => {
|
|||||||
;(modelResolver.resolveModelWithFallback 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<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", () => {
|
describe("Plan agent demote behavior", () => {
|
||||||
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
||||||
// #given
|
// #given
|
||||||
|
|||||||
@ -222,7 +222,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
|||||||
|
|
||||||
agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides(
|
agentConfig["sisyphus-junior"] = createSisyphusJuniorAgentWithOverrides(
|
||||||
pluginConfig.agents?.["sisyphus-junior"],
|
pluginConfig.agents?.["sisyphus-junior"],
|
||||||
config.model as string | undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
if (builderEnabled) {
|
if (builderEnabled) {
|
||||||
|
|||||||
@ -1712,6 +1712,64 @@ describe("sisyphus-task", () => {
|
|||||||
expect(launchInput.model.modelID).toBe("claude-haiku-4-5")
|
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 () => {
|
test("sisyphus-junior model override takes precedence over category model", async () => {
|
||||||
// given - sisyphus-junior override model differs from category default
|
// given - sisyphus-junior override model differs from category default
|
||||||
const { createDelegateTask } = require("./tools")
|
const { createDelegateTask } = require("./tools")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user