import { execFileSync } from "node:child_process"; import { cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { type BuildSystemPromptOptions, CONFIG_DIR_NAME, DefaultPackageManager, type ExtensionUIContext, getAgentDir, getDocsPath, getExamplesPath, getPackageDir, getReadmePath, SettingsManager, VERSION, } from "@earendil-works/pi-coding-agent"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; const extensionRoot = path.resolve(import.meta.dirname, ".."); const repositoryRoot = path.resolve(extensionRoot, "../.."); const ENTRY = fileURLToPath(new URL("../index.ts", import.meta.url)); const PFUI_STATE_KEY = Symbol.for("@bugabinga/pi-ext-pfui/state"); const RENDERED_KEY = "__pfui_contract_rendered__"; const temporaryPaths: string[] = []; let session: PiSession | undefined; // The owned fixture inherits process.env, including any PI_CONTEXT_* the host // shell exports when Pi runs this suite. Clear them so pfui starts from the same // clean slate the fixture sandbox provides. const CLEAN_CONTEXT: Record = { PI_MANAGED_INSTALL_ROOT: undefined, PI_CONTEXT_PACKAGE_DIR: undefined, PI_CONTEXT_AGENT_DIR: undefined, PI_CONTEXT_README_PATH: undefined, PI_CONTEXT_DOCS_PATH: undefined, PI_CONTEXT_EXAMPLES_PATH: undefined, PI_CONTEXT_CHANGELOG_PATH: undefined, PI_CONTEXT_PACKAGE_JSON_PATH: undefined, PI_CONTEXT_VERSION: undefined, PI_CONTEXT_CONFIG_DIR_NAME: undefined, }; function startSession( options: Parameters[0], ): Promise { return createPiSession({ ...options, env: { ...CLEAN_CONTEXT, ...options.env }, }); } // A later extension that captures the rendered system prompt through the public // before_agent_start event. The owned fixture loads extensions by path only, so // this replaces the original inline `observePrompt` factory. const OBSERVE_PROMPT = `export default function (pi) { pi.on("before_agent_start", (event) => { globalThis["${RENDERED_KEY}"] = event.systemPrompt; }); } `; // A later extension that contributes general and tool guidelines, matching the // original inline `contributeGuidelines` factory. const CONTRIBUTE_GUIDELINES = `export default function (pi) { pi.on("before_agent_start", (event) => { event.systemPromptOptions.promptGuidelines.push("Late general guidance"); event.systemPromptOptions.toolGuidelines.read = ["Late read guidance"]; }); } `; // A later extension that appends to the prompt, matching the original inline // `appendLater` factory. const APPEND_LATER = `export default function (pi) { pi.on("before_agent_start", (event) => ({ systemPrompt: \`\${event.systemPrompt}\\n\\nLATER EXTENSION\`, })); } `; function tempDirectory(prefix: string): string { const directory = mkdtempSync(path.join(tmpdir(), prefix)); temporaryPaths.push(directory); return directory; } function writeExtension(body: string): string { const directory = tempDirectory("pfui-ext-"); const file = path.join(directory, "ext.ts"); writeFileSync(file, body); return file; } function copyPfuiEntrypoint(directory: string): void { cpSync( path.join(extensionRoot, "index.ts"), path.join(directory, "index.ts"), ); mkdirSync(path.join(directory, "src"), { recursive: true }); cpSync( path.join(extensionRoot, "src", "pi-ext-debug.ts"), path.join(directory, "src", "pi-ext-debug.ts"), ); cpSync( path.join(extensionRoot, "src", "debug.ts"), path.join(directory, "src", "debug.ts"), ); symlinkSync( path.join(repositoryRoot, "node_modules"), path.join(directory, "node_modules"), "junction", ); } function copiedPfui(prefix: string): string { const directory = tempDirectory(prefix); copyPfuiEntrypoint(directory); return directory; } function baseOptions(): BuildSystemPromptOptions { return { cwd: "/work/project", selectedTools: ["read"], toolSnippets: { read: "Read files" }, promptGuidelines: [], }; } async function emittedOptions( options: BuildSystemPromptOptions, ): Promise { if (!session) throw new Error("Test session is not initialized"); const result = await session.session.extensionRunner.emitBeforeAgentStart( { messageType: "user", prompt: "user prompt" }, options, ); return result.systemPromptOptions; } function pfuiWarningState(): | { warnings: Set; deliveredWarnings: Set } | undefined { return ( globalThis as unknown as Record< symbol, { warnings: Set; deliveredWarnings: Set } | undefined > )[PFUI_STATE_KEY]; } function recordingUi(sink: unknown[][]): ExtensionUIContext { return { select: async () => undefined, confirm: async () => false, input: async () => undefined, notify: (...args: unknown[]) => { sink.push(args); }, onTerminalInput: () => () => {}, setStatus: () => {}, setWorkingMessage: () => {}, setWorkingVisible: () => {}, setWorkingIndicator: () => {}, setHiddenThinkingLabel: () => {}, setWidget: () => {}, setFooter: () => {}, setHeader: () => {}, setTitle: () => {}, custom: async () => undefined, pasteToEditor: () => {}, setEditorText: () => {}, getEditorText: () => "", editor: async () => undefined, addAutocompleteProvider: () => {}, setEditorComponent: () => {}, getEditorComponent: () => undefined, getAllThemes: () => [], getTheme: () => undefined, setTheme: () => ({ success: false, error: "UI not available" }), getToolsExpanded: () => false, setToolsExpanded: () => {}, } as unknown as ExtensionUIContext; } // The owned fixture binds in print mode with a no-op UI and delivers pfui's // warnings to it during bind, which marks them delivered. Re-arm delivery and // install a recording UI so a subsequent lifecycle emit re-delivers the same // warnings for assertion, capturing the delivered notify calls. function captureNotifications(active: PiSession): unknown[][] { const sink: unknown[][] = []; pfuiWarningState()?.deliveredWarnings.clear(); active.session.extensionRunner.setUIContext(recordingUi(sink), "print"); return sink; } function renderedPrompt(): string { return String((globalThis as Record)[RENDERED_KEY] ?? ""); } afterEach(async () => { await session?.dispose(); session = undefined; vi.restoreAllMocks(); delete (globalThis as Record)[PFUI_STATE_KEY]; delete (globalThis as Record)[RENDERED_KEY]; for (const directory of temporaryPaths.splice(0)) { rmSync(directory, { recursive: true, force: true }); } }); describe("pfui contract", () => { it("writes enabled lifecycle debug records in an isolated sandbox", async () => { const root = mkdtempSync(path.join(tmpdir(), "pi-ext-pfui-debug-")); try { session = await startSession({ extensionPaths: [ENTRY], cwd: root, env: { PI_PFUI_DEBUG: "1", XDG_STATE_HOME: path.join(root, "state"), }, }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await emittedOptions({ ...baseOptions(), customPrompt: "CUSTOM" }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); const events = readdirSync( path.join(root, "state", "pi-ext", "debug", "pfui"), ).flatMap((file) => readFileSync( path.join(root, "state", "pi-ext", "debug", "pfui", file), "utf8", ) .trim() .split("\n") .map((line) => JSON.parse(line)), ); expect(events).toEqual( expect.arrayContaining([ expect.objectContaining({ extension: "pfui", event: "session.start", }), expect.objectContaining({ extension: "pfui", event: "session.shutdown", }), expect.objectContaining({ event: "prompt.apply", outcome: "custom_prompt", }), ]), ); } finally { await session?.dispose(); session = undefined; rmSync(root, { recursive: true, force: true }); } }); it("loads through Pi once, first in the generated package list, while the workspace root exposes nothing", async () => { session = await startSession({ extensionPaths: [ENTRY] }); expect(session.extensions).toHaveLength(1); const manifest = JSON.parse( readFileSync(path.join(repositoryRoot, "package.json"), "utf8"), ) as { pi?: unknown }; expect(manifest.pi).toEqual({}); const packageManager = new DefaultPackageManager({ cwd: session.cwd, agentDir: session.agentDir, settingsManager: SettingsManager.inMemory(), }); const resolved = await packageManager.resolveExtensionSources( [repositoryRoot], { temporary: true }, ); expect(resolved.extensions).toEqual([]); // Pi loads one generated package per extension; pfui must come first so it // owns the system prompt before other extensions contribute sections. const entries = JSON.parse( execFileSync( process.execPath, ["--no-warnings", "scripts/build/settings-entries.ts"], { cwd: repositoryRoot, encoding: "utf8" }, ), ) as string[]; const suffix = (entry: string) => entry.split(path.sep).slice(-2).join("/"); expect(suffix(entries[0] ?? "")).toBe("dist/pfui"); expect(suffix(entries[1] ?? "")).toBe("dist/footer"); expect( entries.filter((entry) => suffix(entry) === "dist/pfui"), ).toHaveLength(1); expect(new Set(entries).size).toBe(entries.length); }); it("composes the fallback with dynamic tools and deduplicated guidelines", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const options = baseOptions(); options.selectedTools = ["write", "read", "write"]; options.toolSnippets = { read: "Read current files", write: "Write complete files", inactive: "Must stay hidden", }; options.promptGuidelines = [ " Keep paths explicit. ", "Keep paths explicit.", "", "Verify changes.", ]; const result = await emittedOptions(options); const identity = readFileSync( path.join(extensionRoot, "SYSTEM.md"), "utf8", ).trim(); expect(result.customPrompt).toBe(identity); expect(result.sections?.["tools"]).toContain( "- write: Write complete files\n- read: Read current files", ); expect(result.sections?.["tools"]).not.toContain("inactive"); expect(result.sections?.["general_guidelines"]).toBe( "Keep paths explicit.\nVerify changes.", ); expect(result.sections?.["tool_guidelines"]).toBe(""); }); it.each([{ hiddenTools: [] }, { hiddenTools: ["read", "bash"] }])( "respects hiddenTools $hiddenTools without warning", async ({ hiddenTools }) => { session = await startSession({ extensionPaths: [ writeExtension(`export default function (pi) { pi.on("before_agent_start", (event) => { event.systemPromptOptions.hiddenTools = ${JSON.stringify(hiddenTools)}; }); }`), ENTRY, ], }); const result = await emittedOptions({ ...baseOptions(), selectedTools: ["read", "bash", "codemode"], toolSnippets: { read: "Read files", bash: "Run shell", codemode: "Call tools indirectly", }, toolGuidelines: { read: ["Read guidance"], bash: ["Shell guidance"], codemode: ["Codemode guidance"], }, }); const sections = result.sections; expect(sections?.["tools"]).toContain("- codemode:"); expect(sections?.["tool_guidelines"]).toContain("Codemode guidance"); if (hiddenTools.length) { expect(sections?.["tools"]).not.toContain("- read:"); expect(sections?.["tools"]).not.toContain("- bash:"); expect(sections?.["general_guidelines"]).toBe(""); expect(sections?.["tool_guidelines"]).not.toContain("Read guidance"); expect(sections?.["tool_guidelines"]).not.toContain("Shell guidance"); } else { expect(sections?.["tools"]).toContain("- read:"); expect(sections?.["tools"]).toContain("- bash:"); expect(sections?.["general_guidelines"]).toContain("Use bash"); expect(sections?.["tool_guidelines"]).toContain("Read guidance"); expect(sections?.["tool_guidelines"]).toContain("Shell guidance"); } expect([...(pfuiWarningState()?.warnings ?? [])]).not.toEqual( expect.arrayContaining([expect.stringContaining('"hiddenTools"')]), ); }, ); it("renders guidelines contributed by later extensions", async () => { session = await startSession({ extensionPaths: [ ENTRY, writeExtension(CONTRIBUTE_GUIDELINES), writeExtension(OBSERVE_PROMPT), ], }); await emittedOptions(baseOptions()); expect(renderedPrompt()).toContain(` Late general guidance `); expect(renderedPrompt()).toContain(` Late read guidance `); }); it("passes a configured custom prompt through unchanged", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const options = { ...baseOptions(), customPrompt: "CUSTOM" }; const result = await emittedOptions(options); expect(result.customPrompt).toBe("CUSTOM"); expect(result.sections?.["tools"]).toBeUndefined(); expect(result.sections?.["general_guidelines"]).toBeUndefined(); expect(result.sections?.["tool_guidelines"]).toBeUndefined(); }); it("leaves the fallback available for later extension composition", async () => { session = await startSession({ extensionPaths: [ENTRY, writeExtension(APPEND_LATER)], }); const result = await emittedOptions(baseOptions()); expect(result.forceSystemPrompt).toContain( "You are the spirit in the machine.\nBound inside Pi, give the user's will form.", ); expect(result.forceSystemPrompt).toContain(""); expect(result.forceSystemPrompt).not.toContain(""); expect(result.forceSystemPrompt).toMatch(/<\/tools>\n\nLATER EXTENSION$/); }); it("preserves Pi prompt section formatting and normalizes cwd", async () => { session = await startSession({ extensionPaths: [ENTRY, writeExtension(OBSERVE_PROMPT)], }); const skill = session.session.resourceLoader .getSkills() .skills.find( (candidate: { name: string }) => candidate.name === "pi-knowledge", ); expect(skill).toBeDefined(); const options: BuildSystemPromptOptions = { ...baseOptions(), cwd: "C:\\work\\project", appendSystemPrompt: "APPENDED", contextFiles: [{ path: "/repo/AGENTS.md", content: "PROJECT RULE" }], skills: [skill!], }; const result = await emittedOptions(options); const identity = readFileSync( path.join(extensionRoot, "SYSTEM.md"), "utf8", ).trim(); expect(result.customPrompt).toBe(identity); expect(result.appendSystemPrompt).toBe("APPENDED"); expect(result.contextFiles).toEqual([ { path: "/repo/AGENTS.md", content: "PROJECT RULE" }, ]); expect(result.skills).toEqual([skill]); expect(result.sections?.["tools"]).toContain("- read: Read files"); expect(result.sections?.["general_guidelines"]).toBe(""); expect(result.sections?.["tool_guidelines"]).toBe(""); expect(renderedPrompt()).not.toContain(""); expect(renderedPrompt()).not.toContain(""); expect(result.sections?.["cwd"]).toBeUndefined(); expect(result.sections?.["docs"]).toBeUndefined(); }); it("omits skills when neither read nor bash is active", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const skill = session.session.resourceLoader .getSkills() .skills.find( (candidate: { name: string }) => candidate.name === "pi-knowledge", ); const result = await emittedOptions({ ...baseOptions(), selectedTools: ["powershell"], toolSnippets: { powershell: "Run PowerShell" }, skills: [skill!], }); expect(result.skills).toEqual([skill]); expect(result.sections?.["tools"]).not.toContain("pi-knowledge"); expect(result.sections?.["general_guidelines"]).toBe( "Use PowerShell for file operations like listing, searching, and finding files", ); expect(result.sections?.["tool_guidelines"]).toBe(""); }); it("separates general and tool guidelines without escaping their text", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const result = await emittedOptions({ ...baseOptions(), selectedTools: ["edit", "angel"], toolSnippets: { edit: "Edit files", angel: "Consult Angel" }, toolGuidelines: { edit: ["Edit carefully", "Shared & text", "Edit carefully"], angel: ["Shared & text", "Ask a focused question"], }, promptGuidelines: ["Global guidance", "Global guidance"], }); expect(result.sections?.["general_guidelines"]).toBe("Global guidance"); expect(result.sections?.["tool_guidelines"]).toBe(` Edit carefully Shared & text Shared & text Ask a focused question `); }); it("discovers the bundled pi-knowledge skill with its contract", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const skill = session.session.resourceLoader .getSkills() .skills.find( (candidate: { name: string }) => candidate.name === "pi-knowledge", ); expect(skill?.filePath).toBe( path.join(extensionRoot, "skills", "pi-knowledge", "SKILL.md"), ); const source = readFileSync(skill!.filePath, "utf8"); expect(source).toContain("Requires Pi."); expect(source).toContain( "questions about Pi and for implementing or changing Pi", ); expect(source).toContain( "Resolve only the needed variables using available tools", ); expect(source).toContain("Never dump the complete environment"); expect(source).toContain("If required variables are missing"); expect(source).not.toMatch(/Bash|PowerShell|active shell/); expect(source).toContain("Read every selected Markdown file completely"); expect(source).toContain("If a mapped file moved or is absent"); }); it("keeps a valid bundled skill when Pi reports only warnings", async () => { const warned = copiedPfui("pfui-warned-"); cpSync( path.join(extensionRoot, "SYSTEM.md"), path.join(warned, "SYSTEM.md"), ); mkdirSync(path.join(warned, "skills", "pi-knowledge"), { recursive: true, }); writeFileSync( path.join(warned, "skills", "pi-knowledge", "SKILL.md"), `---\nname: pi-knowledge\ndescription: "${"x".repeat(1025)}"\n---\n\n# Pi knowledge\n`, ); session = await startSession({ cwd: tempDirectory("pfui-warned-cwd-"), extensionPaths: [path.join(warned, "index.ts")], }); expect( session.session.resourceLoader .getSkills() .skills.some( (candidate: { name: string }) => candidate.name === "pi-knowledge", ), ).toBe(true); }); it("preserves Pi 0.86 tool guidelines and custom sections", async () => { session = await startSession({ extensionPaths: [ENTRY] }); const options: BuildSystemPromptOptions = { ...baseOptions(), toolGuidelines: { read: ["Read before editing."], }, sections: { custom_context: "Keep the change focused.", }, }; const notifications = captureNotifications(session); const result = await emittedOptions(options); expect(notifications).toHaveLength(0); expect(result.sections?.["general_guidelines"]).toBe(""); expect(result.sections?.["tool_guidelines"]).toBe(` Read before editing. `); expect(result.sections?.["custom_context"]).toBe( "Keep the change focused.", ); }); it("exports public Pi context and cleans only unchanged owned values", async () => { session = await startSession({ extensionPaths: [ENTRY], env: { PI_CONTEXT_VERSION: VERSION }, }); const packageDir = getPackageDir(); const expected = { PI_CONTEXT_PACKAGE_DIR: packageDir, PI_CONTEXT_AGENT_DIR: getAgentDir(), PI_CONTEXT_README_PATH: getReadmePath(), PI_CONTEXT_DOCS_PATH: getDocsPath(), PI_CONTEXT_EXAMPLES_PATH: getExamplesPath(), PI_CONTEXT_CHANGELOG_PATH: path.join(packageDir, "CHANGELOG.md"), PI_CONTEXT_PACKAGE_JSON_PATH: path.join(packageDir, "package.json"), PI_CONTEXT_VERSION: VERSION, PI_CONTEXT_CONFIG_DIR_NAME: CONFIG_DIR_NAME, }; for (const [name, value] of Object.entries(expected)) { expect(process.env[name]).toBe(value); } // The original observed these values inside a later extension's // session_shutdown handler, which runs after pfui's cleanup. Reading // process.env immediately after the shutdown emit observes the same state. process.env["PI_CONTEXT_DOCS_PATH"] = "kept-after-shutdown"; await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); expect(process.env["PI_CONTEXT_PACKAGE_DIR"]).toBeUndefined(); expect(process.env["PI_CONTEXT_DOCS_PATH"]).toBe("kept-after-shutdown"); expect(process.env["PI_CONTEXT_VERSION"]).toBe(VERSION); }); it("keeps running Pi context when the managed pointer selects a different release", async () => { const installRoot = tempDirectory("pfui-managed-"); const packageDir = path.join( installRoot, "releases", "9.8.7", "node_modules", "@earendil-works", "pi-coding-agent", ); mkdirSync(path.join(packageDir, "docs"), { recursive: true }); mkdirSync(path.join(packageDir, "examples")); writeFileSync(path.join(installRoot, "current-version"), "9.8.7\n"); writeFileSync( path.join(packageDir, "package.json"), JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: "9.8.7", }), ); writeFileSync(path.join(packageDir, "README.md"), "# Pi"); writeFileSync(path.join(packageDir, "CHANGELOG.md"), "# Changelog"); session = await startSession({ extensionPaths: [ENTRY], env: { PI_MANAGED_INSTALL_ROOT: installRoot }, }); const runningPackage = getPackageDir(); const expected = { PI_CONTEXT_PACKAGE_DIR: runningPackage, PI_CONTEXT_README_PATH: getReadmePath(), PI_CONTEXT_DOCS_PATH: getDocsPath(), PI_CONTEXT_EXAMPLES_PATH: getExamplesPath(), PI_CONTEXT_CHANGELOG_PATH: path.join(runningPackage, "CHANGELOG.md"), PI_CONTEXT_PACKAGE_JSON_PATH: path.join(runningPackage, "package.json"), PI_CONTEXT_VERSION: VERSION, }; for (const [name, value] of Object.entries(expected)) expect(process.env[name]).toBe(value); }); it.each(["missing", "traversal", "metadata"])( "keeps running Pi context despite %s managed pointer errors", async (failure) => { const installRoot = tempDirectory("pfui-managed-invalid-"); if (failure === "traversal") writeFileSync(path.join(installRoot, "current-version"), "../outside"); if (failure === "metadata") { writeFileSync(path.join(installRoot, "current-version"), "9.8.7"); const packageDir = path.join( installRoot, "releases", "9.8.7", "node_modules", "@earendil-works", "pi-coding-agent", ); mkdirSync(packageDir, { recursive: true }); writeFileSync( path.join(packageDir, "package.json"), JSON.stringify({ name: "other", version: "9.8.7" }), ); } session = await startSession({ extensionPaths: [ENTRY], env: { PI_MANAGED_INSTALL_ROOT: installRoot }, }); expect(process.env["PI_CONTEXT_PACKAGE_DIR"]).toBe(getPackageDir()); expect(process.env["PI_CONTEXT_VERSION"]).toBe(VERSION); expect(process.env["PI_CONTEXT_DOCS_PATH"]).toBe(getDocsPath()); const notifications = captureNotifications(session); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); expect(notifications).toEqual([]); }, ); it("preserves a conflicting context variable and warns", async () => { session = await startSession({ extensionPaths: [ENTRY], env: { PI_CONTEXT_VERSION: "external-version" }, }); expect(process.env["PI_CONTEXT_VERSION"]).toBe("external-version"); const notifications = captureNotifications(session); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); expect(notifications).toHaveLength(1); expect(String(notifications[0]?.[0])).toContain( "PI_CONTEXT_VERSION already has a different value", ); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); expect(process.env["PI_CONTEXT_VERSION"]).toBe("external-version"); }); it("omits a missing resolved resource path and warns", async () => { const cwd = tempDirectory("pfui-missing-path-cwd-"); session = await startSession({ extensionPaths: [ENTRY], cwd, // The fixture always creates its agent dir, so point the extension's // resolved agent dir at a missing path to reproduce the omitted resource. env: { PI_CODING_AGENT_DIR: path.join(cwd, "absent-agent") }, }); expect(process.env["PI_CONTEXT_AGENT_DIR"]).toBeUndefined(); const notifications = captureNotifications(session); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); expect(notifications).toHaveLength(1); expect(String(notifications[0]?.[0])).toContain( "PI_CONTEXT_AGENT_DIR was omitted", ); }); it("preserves Pi's prompt when the bundled fallback is missing", async () => { const broken = copiedPfui("pfui-missing-fallback-"); cpSync(path.join(extensionRoot, "skills"), path.join(broken, "skills"), { recursive: true, }); session = await startSession({ cwd: tempDirectory("pfui-missing-fallback-cwd-"), extensionPaths: [path.join(broken, "index.ts")], }); const notifications = captureNotifications(session); const result = await emittedOptions(baseOptions()); expect(result.customPrompt).toBeUndefined(); const warnings = notifications .map((notification) => String(notification[0])) .join("\n"); expect(warnings).toContain("Bundled SYSTEM.md is unavailable"); }); it.each([ ["missing", undefined, "is missing"], ["invalid", "---\nname: pi-knowledge\n---\ninvalid\n", "is invalid"], ])("omits a %s bundled skill and warns", async (_case, source, message) => { const broken = copiedPfui(`pfui-${_case}-skill-`); cpSync( path.join(extensionRoot, "SYSTEM.md"), path.join(broken, "SYSTEM.md"), ); if (source) { mkdirSync(path.join(broken, "skills", "pi-knowledge"), { recursive: true, }); writeFileSync( path.join(broken, "skills", "pi-knowledge", "SKILL.md"), source, ); } session = await startSession({ cwd: tempDirectory(`pfui-${_case}-skill-cwd-`), extensionPaths: [path.join(broken, "index.ts")], }); expect( session.session.resourceLoader .getSkills() .skills.some( (candidate: { name: string }) => candidate.name === "pi-knowledge", ), ).toBe(false); const notifications = captureNotifications(session); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); const warnings = notifications .map((notification) => String(notification[0])) .join("\n"); expect(warnings).toContain(`Bundled pi-knowledge skill ${message}`); }); });