import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness"; const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); const DEFAULTS = ["alt+n", "ctrl+enter", "ctrl+alt+enter"]; type Case = { user?: unknown; project?: unknown; env?: Record; }; describe("notes shortcut settings", () => { let t: TestSession | undefined; let root: string | undefined; afterEach(async () => { await t?.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); vi.restoreAllMocks(); t?.dispose(); t = undefined; if (root) rmSync(root, { recursive: true, force: true }); root = undefined; }); async function start(input: Case) { root = mkdtempSync(path.join(tmpdir(), "pi-ext-notes-settings-")); const write = (dir: string, value: unknown) => { mkdirSync(dir, { recursive: true }); writeFileSync( path.join(dir, "settings.json"), JSON.stringify({ notes: value }), ); }; if ("user" in input) write(path.join(root, ".test-home/.pi/agent"), input.user); if ("project" in input) write(path.join(root, ".pi"), input.project); // Shortcuts load before any session, from process.cwd(). vi.spyOn(process, "cwd").mockReturnValue(root); t = await createTestSession({ cwd: root, extensions: [extensionPath], env: input.env, }); const extension = t.session.extensionRunner.extensions[0]; const before = t.events.uiCallsFor("notify").length; await t.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); const warnings = t.events .uiCallsFor("notify") .slice(before) .filter((call) => call.args[1] === "warning") .map((call) => call.args[0]); return { shortcuts: [...extension.shortcuts.keys()], warnings }; } it.each([ ["default", {}, DEFAULTS], [ "user", { user: { toggleShortcut: "alt+j", addFromEditorShortcut: "alt+k", sendPickerShortcut: "alt+l", }, }, ["alt+j", "alt+k", "alt+l"], ], [ "env over user", { user: { toggleShortcut: "alt+j" }, env: { PI_NOTES_TOGGLE_SHORTCUT: "alt+x", PI_NOTES_ADD_FROM_EDITOR_SHORTCUT: "alt+y", PI_NOTES_SEND_PICKER_SHORTCUT: "alt+z", }, }, ["alt+x", "alt+y", "alt+z"], ], [ "trusted project ignored", { user: { toggleShortcut: "alt+j" }, project: { toggleShortcut: "alt+p", sendPickerShortcut: "alt+q" }, }, ["alt+j", "ctrl+enter", "ctrl+alt+enter"], ], [ "swapped shortcuts stay unique", { user: { toggleShortcut: "ctrl+enter", addFromEditorShortcut: "alt+n", }, }, ["ctrl+enter", "alt+n", "ctrl+alt+enter"], ], ] satisfies [string, Case, string[]][])( "%s", async (_name, input, expected) => { const { shortcuts, warnings } = await start(input); expect(shortcuts).toEqual(expected); expect(warnings).toEqual([]); }, ); it.each([ [ "wrong type", { user: { toggleShortcut: 1 } }, [ "Invalid notes.toggleShortcut from user settings: must be a non-empty string", ], ], [ "blank user value", { user: { sendPickerShortcut: " " } }, [ "Invalid notes.sendPickerShortcut from user settings: must be a non-empty string", ], ], [ "empty env does not fall through to user", { user: { addFromEditorShortcut: "alt+k" }, env: { PI_NOTES_ADD_FROM_EDITOR_SHORTCUT: "" }, }, [ "Invalid notes.addFromEditorShortcut from environment variable PI_NOTES_ADD_FROM_EDITOR_SHORTCUT: must be a non-empty string", ], ], [ "duplicates", { user: { toggleShortcut: "alt+j", sendPickerShortcut: "alt+j" } }, ["Duplicate notes shortcuts: alt+j, ctrl+enter, alt+j"], ], ] satisfies [string, Case, string[]][])( "%s warns and keeps defaults", async (_name, input, expected) => { const { shortcuts, warnings } = await start(input); expect(shortcuts).toEqual(DEFAULTS); expect(warnings).toEqual(expected); }, ); });