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 { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); type Case = { user?: unknown; project?: unknown }; interface NotifyCall { args: unknown[]; } /** * Install a recording UI context so masks' `ctx.ui.notify` warnings are * captured as notify calls. */ function installRecordingUi(session: PiSession): NotifyCall[] { const calls: NotifyCall[] = []; session.session.extensionRunner.setUIContext( { ...session.session.extensionRunner.createContext().ui, notify: (...args: unknown[]) => calls.push({ args }), }, "print", ); return calls; } const mask = (name: string, shortcut: string) => ({ name, model: "openai/gpt-4o", thinkingLevel: "low", shortcut, }); describe("masks settings resolution", () => { let session: PiSession | undefined; let root: string | undefined; afterEach(async () => { vi.restoreAllMocks(); await session?.dispose(); session = undefined; if (root) rmSync(root, { recursive: true, force: true }); root = undefined; }); async function start(input: Case) { root = mkdtempSync(path.join(tmpdir(), "pi-ext-masks-settings-")); if ("project" in input) { mkdirSync(path.join(root, ".pi"), { recursive: true }); writeFileSync( path.join(root, ".pi", "settings.json"), JSON.stringify({ masks: input.project }), ); } // Masks load before any session, from process.cwd(). vi.spyOn(process, "cwd").mockReturnValue(root); session = await createPiSession({ cwd: root, extensionPaths: [extensionPath], ...("user" in input ? { userSettings: { masks: input.user } } : {}), }); const notifications = installRecordingUi(session); // Masks emit configuration warnings on session start. await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); const extension = session.extensions[0]; if (!extension) throw new Error("missing test extension"); const warnings = notifications .filter((call) => call.args[1] === "warning") .map((call) => call.args[0]); return { shortcuts: [...extension.shortcuts.keys()], warnings }; } it.each([ ["default", {}, ["alt+m"]], [ "user", { user: { pickerShortcut: "alt+p", items: [mask("a", "alt+1")] } }, ["alt+p", "alt+1"], ], [ "trusted project ignored", { user: { items: [mask("a", "alt+1")] }, project: { pickerShortcut: "alt+p", items: [mask("b", "alt+2")] }, }, ["alt+m", "alt+1"], ], ] satisfies [string, Case, string[]][])( "%s", async (_name, input, expected) => { const { shortcuts, warnings } = await start(input); expect(shortcuts).toEqual(expected); expect(warnings).toEqual([]); }, ); it("reports invalid user settings without falling back to project", async () => { const { shortcuts, warnings } = await start({ user: "alt+p", project: { items: [mask("b", "alt+2")] }, }); expect(shortcuts).toEqual(["alt+m"]); expect(warnings).toEqual(["masks must be an object"]); }); });