diff --git a/packages/coding-agent/test/input-controller-slash-history.test.ts b/packages/coding-agent/test/input-controller-slash-history.test.ts new file mode 100644 index 000000000..82f63edb3 --- /dev/null +++ b/packages/coding-agent/test/input-controller-slash-history.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it, vi } from "bun:test"; +import { InputController } from "@oh-my-pi/pi-coding-agent/modes/controllers/input-controller"; +import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; + +// Drives the real editor submit handler through the builtin slash dispatch +// path. Before #3148 only a handful of commands recorded their text (each +// added it inside its own handler); everything else returned `true` from +// executeBuiltinSlashCommand and the controller returned before any +// addToHistory call. The fix centralizes recording after dispatch, with a +// secret filter (shouldSkipHistory) for credential-bearing commands. +function makeCtx() { + const addToHistory = vi.fn(); + const handleMCPCommand = vi.fn(async () => {}); + let text = ""; + const editor = { + onSubmit: undefined as undefined | ((t: string) => Promise), + getText: () => text, + setText: (t: string) => { + text = t; + }, + addToHistory, + pendingImages: [] as unknown[], + pendingImageLinks: [] as unknown[], + clearDraft(historyText?: string) { + if (historyText !== undefined) addToHistory(historyText); + text = ""; + }, + }; + const ctx = { + editor, + session: { + isStreaming: false, + isCompacting: false, + queuedMessageCount: 0, + extensionRunner: undefined, + }, + focusedAgentId: undefined, + collabGuest: undefined, + handleHotkeysCommand: vi.fn(), + handleMCPCommand, + showStatus: vi.fn(), + ui: { requestRender: vi.fn() }, + } as unknown as InteractiveModeContext; + return { ctx, editor, addToHistory, handleMCPCommand }; +} + +function controllerFor(ctx: InteractiveModeContext) { + const controller = new InputController(ctx); + controller.setupEditorSubmitHandler(); + return controller; +} + +describe("input controller — slash command history (#3148)", () => { + it("records a plain handled command (/hotkeys) that has no per-handler history call", async () => { + const { ctx, editor, addToHistory } = makeCtx(); + controllerFor(ctx); + + await editor.onSubmit?.("/hotkeys"); + + expect(addToHistory).toHaveBeenCalledWith("/hotkeys"); + }); + + it("records a non-secret /mcp subcommand", async () => { + const { ctx, editor, addToHistory, handleMCPCommand } = makeCtx(); + controllerFor(ctx); + + await editor.onSubmit?.("/mcp list"); + + expect(handleMCPCommand).toHaveBeenCalledWith("/mcp list"); + expect(addToHistory).toHaveBeenCalledWith("/mcp list"); + }); + + it("does NOT record /mcp add with a --token (would leak the bearer token)", async () => { + const { ctx, editor, addToHistory, handleMCPCommand } = makeCtx(); + controllerFor(ctx); + + await editor.onSubmit?.("/mcp add srv --url http://x --token sk-secret123"); + + // Command still executes... + expect(handleMCPCommand).toHaveBeenCalledWith("/mcp add srv --url http://x --token sk-secret123"); + // ...but the secret-bearing text is kept out of recallable history. + expect(addToHistory).not.toHaveBeenCalled(); + }); +});