diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts new file mode 100644 index 000000000..4b5108add --- /dev/null +++ b/src/plugin/chat-message.test.ts @@ -0,0 +1,118 @@ +import { describe, test, expect } from "bun:test" + +import { createChatMessageHandler } from "./chat-message" + +type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } +type ChatMessageHandlerOutput = { message: Record; parts: ChatMessagePart[] } + +function createMockHandlerArgs(overrides?: { + pluginConfig?: Record + shouldOverride?: boolean +}) { + const appliedSessions: string[] = [] + return { + ctx: { client: { tui: { showToast: async () => {} } } } as any, + pluginConfig: (overrides?.pluginConfig ?? {}) as any, + firstMessageVariantGate: { + shouldOverride: () => overrides?.shouldOverride ?? false, + markApplied: (sessionID: string) => { appliedSessions.push(sessionID) }, + }, + hooks: { + stopContinuationGuard: null, + keywordDetector: null, + claudeCodeHooks: null, + autoSlashCommand: null, + startWork: null, + ralphLoop: null, + } as any, + _appliedSessions: appliedSessions, + } +} + +function createMockInput(agent?: string, model?: { providerID: string; modelID: string }) { + return { + sessionID: "test-session", + agent, + model, + } +} + +function createMockOutput(variant?: string): ChatMessageHandlerOutput { + const message: Record = {} + if (variant !== undefined) { + message["variant"] = variant + } + return { message, parts: [] } +} + +describe("createChatMessageHandler - first message variant", () => { + test("first message: sets variant from fallback chain when user has no selection", async () => { + //#given - first message, no user-selected variant, hephaestus with medium in chain + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput() // no variant set + + //#when + await handler(input, output) + + //#then - should set variant from fallback chain + expect(output.message["variant"]).toBeDefined() + }) + + test("first message: preserves user-selected variant when already set", async () => { + //#given - first message, user already selected "xhigh" variant in OpenCode UI + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") // user selected xhigh + + //#when + await handler(input, output) + + //#then - user's xhigh must be preserved, not overwritten to "medium" + expect(output.message["variant"]).toBe("xhigh") + }) + + test("first message: preserves user-selected 'high' variant", async () => { + //#given - user selected "high" variant + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("high") + + //#when + await handler(input, output) + + //#then + expect(output.message["variant"]).toBe("high") + }) + + test("subsequent message: does not override existing variant", async () => { + //#given - not first message, variant already set + const args = createMockHandlerArgs({ shouldOverride: false }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") + + //#when + await handler(input, output) + + //#then + expect(output.message["variant"]).toBe("xhigh") + }) + + test("first message: marks gate as applied regardless of variant presence", async () => { + //#given - first message with user-selected variant + const args = createMockHandlerArgs({ shouldOverride: true }) + const handler = createChatMessageHandler(args) + const input = createMockInput("hephaestus", { providerID: "openai", modelID: "gpt-5.3-codex" }) + const output = createMockOutput("xhigh") + + //#when + await handler(input, output) + + //#then - gate should still be marked as applied + expect(args._appliedSessions).toContain("test-session") + }) +}) diff --git a/src/plugin/chat-message.ts b/src/plugin/chat-message.ts index 8cc1b394d..e67203207 100644 --- a/src/plugin/chat-message.ts +++ b/src/plugin/chat-message.ts @@ -56,12 +56,14 @@ export function createChatMessageHandler(args: { const message = output.message if (firstMessageVariantGate.shouldOverride(input.sessionID)) { - const variant = - input.model && input.agent - ? resolveVariantForModel(pluginConfig, input.agent, input.model) - : resolveAgentVariant(pluginConfig, input.agent) - if (variant !== undefined) { - message["variant"] = variant + if (message["variant"] === undefined) { + const variant = + input.model && input.agent + ? resolveVariantForModel(pluginConfig, input.agent, input.model) + : resolveAgentVariant(pluginConfig, input.agent) + if (variant !== undefined) { + message["variant"] = variant + } } firstMessageVariantGate.markApplied(input.sessionID) } else {