import { chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import os from "node:os"; import path from "node:path"; import { type ExtensionCommandContext, initTheme, } from "@earendil-works/pi-coding-agent"; import { visibleWidth } from "@earendil-works/pi-tui"; import { afterEach, describe, expect, it } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness.ts"; import { inspectCandidates, showInspection } from "../inspect.ts"; import { matchSkill } from "../match.ts"; const extension = path.resolve(import.meta.dirname, "../index.ts"); function skill(agentDir: string, name: string, conditions: string): void { const dir = path.join(agentDir, "skillz", name); mkdirSync(dir, { recursive: true }); writeFileSync( path.join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: Skill ${name}\n${conditions}---\n\n# ${name}\n`, ); } describe("skillz conditions", () => { const cwd = path.join(os.tmpdir(), "skillz-cwd"); it("combines fields with AND and values within fields with OR", () => { expect( matchSkill({ os: ["mac", "lnx"], host: ["a", "b"] }, cwd, "linux", "b") .matches, ).toBe(true); expect( matchSkill({ os: "win32", host: "b" }, cwd, "linux", "b").matches, ).toBe(false); expect(matchSkill({ host: "B" }, cwd, "linux", "b").matches).toBe(false); }); it("rejects missing and malformed conditions", () => { for (const value of [ {}, { os: "linux" }, { os: [] }, { host: 4 }, { path: "../up" }, { path: "C:/x" }, { path: "foo\\bar" }, { path: "." }, { which: [] }, { which: "gh --version" }, { which: "../gh" }, ]) expect(matchSkill(value, cwd).error).toBeDefined(); }); it("validates every present condition before matching", () => { expect( matchSkill( { os: "mac", host: [], path: "../outside" }, cwd, "linux", "x", ), ).toEqual({ matches: false, error: "invalid host condition" }); expect( matchSkill( { os: "mac", host: "x", path: "../outside" }, cwd, "linux", "x", ), ).toEqual({ matches: false, error: "invalid path condition" }); expect( matchSkill({ os: "mac", host: "x", path: ".jj" }, cwd, "linux", "x") .matches, ).toBe(false); }); it("supports explicit any and all for every field", () => { const any = matchSkill( { os: { any: ["mac", "lnx"] }, host: { all: ["desktop", "other"] } }, cwd, "linux", "desktop", ); expect(any.matches).toBe(false); expect(any.checks?.map((check) => [check.mode, check.matches])).toEqual([ ["any", true], ["all", false], ]); expect( matchSkill({ os: { all: ["lnx", "lnx"] } }, cwd, "linux").matches, ).toBe(true); expect( matchSkill({ os: { all: ["mac", "lnx"] } }, cwd, "linux").matches, ).toBe(false); expect( matchSkill( { host: { any: ["other", "desktop"] } }, cwd, "linux", "desktop", ).matches, ).toBe(true); for (const condition of [ { os: { any: [] } }, { host: { all: [] } }, { path: { any: ["x", 5] } }, { which: { any: "gh" } }, { env: { all: ["OK"], any: ["NO"] } }, { os: { nope: ["lnx"] } }, ]) expect(matchSkill(condition, cwd).error).toBeDefined(); }); it("matches nonempty env vars and retains names, never values", () => { const secret = ["skillz-secret", "DO-NOT-PRINT"].join("-"); const names = [ "SKILLZ_TEST_PRESENT", "SKILLZ_TEST_EMPTY", "SKILLZ_TEST_MISSING", ]; const previous = names.map((name) => process.env[name]); try { process.env.SKILLZ_TEST_PRESENT = secret; process.env.SKILLZ_TEST_EMPTY = ""; delete process.env.SKILLZ_TEST_MISSING; const present = matchSkill({ env: "SKILLZ_TEST_PRESENT" }, cwd); expect(present.matches).toBe(true); expect(JSON.stringify(present)).not.toContain(secret); expect(matchSkill({ env: "SKILLZ_TEST_EMPTY" }, cwd).matches).toBe(false); expect(matchSkill({ env: "SKILLZ_TEST_MISSING" }, cwd).matches).toBe( false, ); expect( matchSkill({ env: ["SKILLZ_TEST_EMPTY", "SKILLZ_TEST_PRESENT"] }, cwd) .matches, ).toBe(true); expect( matchSkill( { env: { any: ["SKILLZ_TEST_EMPTY", "SKILLZ_TEST_PRESENT"] } }, cwd, ).matches, ).toBe(true); expect( matchSkill( { env: { all: ["SKILLZ_TEST_EMPTY", "SKILLZ_TEST_PRESENT"] } }, cwd, ).matches, ).toBe(false); expect( matchSkill( { env: { all: ["SKILLZ_TEST_PRESENT"] }, os: "mac" }, cwd, "linux", ).matches, ).toBe(false); for (const value of [ "", "1INVALID", "A=B", ["SKILLZ_TEST_PRESENT", "bad-name"], ]) expect(matchSkill({ env: value }, cwd).error).toBe( "invalid env condition", ); } finally { for (const [index, name] of names.entries()) { if (previous[index] === undefined) delete process.env[name]; else process.env[name] = previous[index]; } } }); it.skipIf(process.platform === "win32")( "checks executable files on PATH without running them", () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-which-")); const first = path.join(root, "first"); const second = path.join(root, "second"); const originalPath = process.env.PATH; try { mkdirSync(first); mkdirSync(second); writeFileSync(path.join(first, "gh"), ""); writeFileSync(path.join(second, "gh"), ""); chmodSync(path.join(first, "gh"), 0o644); chmodSync(path.join(second, "gh"), 0o755); symlinkSync("gh", path.join(second, "gh-link")); mkdirSync(path.join(second, "directory")); process.env.PATH = [first, second].join(path.delimiter); expect(matchSkill({ which: "gh" }, root).matches).toBe(true); expect(matchSkill({ which: "gh-link" }, root).matches).toBe(true); expect( matchSkill({ which: { all: ["gh", "gh-link"] } }, root).matches, ).toBe(true); expect( matchSkill({ which: { all: ["gh", "absent"] } }, root).matches, ).toBe(false); expect(matchSkill({ which: "directory" }, root).matches).toBe(false); expect(matchSkill({ which: ["missing", "gh"] }, root).matches).toBe( true, ); expect(matchSkill({ which: "gh", path: "missing" }, root).matches).toBe( false, ); process.env.PATH = first; expect(matchSkill({ which: "gh" }, root).matches).toBe(false); } finally { if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; rmSync(root, { recursive: true, force: true }); } }, ); it("resolves Windows PATHEXT and rejects directories", () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-win-which-")); const originalPaths = Object.entries(process.env).filter( ([key]) => key.toLowerCase() === "path", ); const originalPathext = process.env.PATHEXT; try { writeFileSync(path.join(root, "gh.exe"), ""); mkdirSync(path.join(root, "directory.EXE")); for (const [key] of originalPaths) delete process.env[key]; process.env.Path = `;"${root}";`; process.env.PATHEXT = ".EXE;.CMD"; expect(matchSkill({ which: "gh" }, root, "win32").matches).toBe(true); expect( matchSkill({ which: path.join(root, "gh.exe") }, root, "win32").matches, ).toBe(false); expect(matchSkill({ which: "directory" }, root, "win32").matches).toBe( false, ); process.env.PATHEXT = ".CMD"; expect(matchSkill({ which: "gh" }, root, "win32").matches).toBe(false); } finally { for (const [key] of Object.entries(process.env)) if (key.toLowerCase() === "path") delete process.env[key]; for (const [key, value] of originalPaths) process.env[key] = value; if (originalPathext === undefined) delete process.env.PATHEXT; else process.env.PATHEXT = originalPathext; rmSync(root, { recursive: true, force: true }); } }); it("matches exact cwd markers and rejects symlink components", () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-markers-")); try { writeFileSync(path.join(root, ".jj"), ""); mkdirSync(path.join(root, "nested")); writeFileSync(path.join(root, "nested", "marker"), ""); symlinkSync("nested", path.join(root, "linked")); expect(matchSkill({ path: ".jj" }, root).matches).toBe(true); expect( matchSkill({ path: { all: [".jj", "nested/marker"] } }, root).matches, ).toBe(true); expect( matchSkill({ path: { all: [".jj", "absent"] } }, root).matches, ).toBe(false); expect( matchSkill({ path: ["missing", "nested/marker"] }, root).matches, ).toBe(true); expect( matchSkill({ path: ".jj" }, path.join(root, "nested")).matches, ).toBe(false); expect(matchSkill({ path: "linked/marker" }, root).matches).toBe(false); } finally { rmSync(root, { recursive: true, force: true }); } }); }); describe("skillz inspection", () => { it("renders a bounded navigable TUI view without writing to chat", async () => { initTheme("default", false); let closed = false; const rows = Array.from({ length: 25 }, (_, index) => ({ name: `skill-${index}`, status: "excluded" as const, detail: `host mismatch ${index}`, })); const context = { mode: "tui", ui: { custom: async (factory: (...args: unknown[]) => unknown) => { const view = (await factory( { requestRender: () => {} }, { fg: (_color: string, text: string) => text, bold: (text: string) => text, }, {}, () => { closed = true; }, )) as { render(width: number): string[]; handleInput(data: string): void; }; for (const width of [24, 80]) { const lines = view.render(width); expect(lines.length).toBeLessThan(20); expect(lines.every((line) => visibleWidth(line) <= width)).toBe( true, ); } view.handleInput("\u001b[B"); expect(view.render(80).join("\n")).toContain("host mismatch 1"); view.handleInput("\u001b"); }, }, } as unknown as ExtensionCommandContext; await showInspection(context, rows, true); expect(closed).toBe(true); }); it("reports registry status by path, evidence, and env presence only", () => { const secret = ["skillz-secret", "DO-NOT-PRINT"].join("-"); const previous = process.env.SKILLZ_TEST_PRESENT; try { process.env.SKILLZ_TEST_PRESENT = secret; const result = matchSkill( { env: { all: ["SKILLZ_TEST_PRESENT"] } }, os.tmpdir(), ); const candidates = [ { name: "loaded", path: "/skillz/loaded/SKILL.md", submitted: true, checks: result.checks, }, { name: "missing", path: "/skillz/missing/SKILL.md", submitted: true }, { name: "stale", path: "/skillz/stale/SKILL.md", submitted: false, reason: "host mismatch", }, { name: "collision", path: "/skillz/collision/SKILL.md", submitted: false, reason: "name collision", }, ]; const commands = [ { name: "skill:loaded", source: "skill" as const, sourceInfo: { path: candidates[0].path, source: "user", scope: "user" as const, origin: "top-level" as const, }, }, { name: "skill:missing", source: "skill" as const, sourceInfo: { path: "/native/missing/SKILL.md", source: "user", scope: "user" as const, origin: "top-level" as const, }, }, { name: "skill:stale", source: "skill" as const, sourceInfo: { path: "/skillz/stale/SKILL.md", source: "user", scope: "user" as const, origin: "top-level" as const, }, }, ]; const rows = inspectCandidates(candidates, commands); expect(rows.map((row) => [row.name, row.status])).toEqual([ ["collision", "excluded"], ["loaded", "loaded"], ["missing", "excluded"], ["stale", "loaded"], ]); expect(rows.find((row) => row.name === "loaded")?.detail).toContain( "SKILLZ_TEST_PRESENT present", ); expect(rows.find((row) => row.name === "missing")?.detail).toContain( "name claimed", ); expect(rows.find((row) => row.name === "stale")?.detail).toContain( "already registered by Pi despite skillz exclusion", ); expect(JSON.stringify(rows)).not.toContain(secret); } finally { if (previous === undefined) delete process.env.SKILLZ_TEST_PRESENT; else process.env.SKILLZ_TEST_PRESENT = previous; } }); }); describe("skillz runtime", () => { let t: TestSession | undefined; const roots: string[] = []; afterEach(() => { t?.dispose(); t = undefined; for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); it("loads eligible global skills into native registry and /skill commands", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-runtime-")); roots.push(root); const agentDir = path.join(root, "agent"); mkdirSync(path.join(root, ".jj")); skill(agentDir, "matched", "path: .jj\n"); skill(agentDir, "wrong-host", "host: definitely-not-this-host\n"); skill( agentDir, "manual", "os: [lnx, mac, win32]\ndisable-model-invocation: true\n", ); skill(agentDir, "unconditional", ""); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_CODING_AGENT_DIR: agentDir }, }); const names = t.session.resourceLoader .getSkills() .skills.map((entry) => entry.name); expect(names).toContain("matched"); expect(names).toContain("manual"); expect(t.session.systemPrompt).not.toContain("manual"); expect(names).not.toContain("wrong-host"); expect(names).not.toContain("unconditional"); expect( t.session.extensionRunner.extensions.map((entry) => entry.path), ).toContain(extension); const expand = t.session as unknown as { _expandSkillCommand(text: string): string; }; expect(expand._expandSkillCommand("/skill:matched")).toContain( ' { const root = mkdtempSync(path.join(os.tmpdir(), "pi-ext-skillz-debug-")); roots.push(root); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_SKILLZ_DEBUG: "1" }, }); const runner = t.session.extensionRunner; await runner.emit({ type: "session_start", reason: "startup" }); await runner.emit({ type: "session_shutdown", reason: "quit" }); const stateHome = t.env.XDG_STATE_HOME; if (!stateHome) throw new Error("missing sandbox state home"); const directory = path.join(stateHome, "pi-ext", "debug", "skillz"); const [file] = readdirSync(directory); const events = readFileSync(path.join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line).event); expect(events).toEqual( expect.arrayContaining(["session.start", "session.shutdown"]), ); }); it("adds no prompt text without matching skills", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-context-")); roots.push(root); const agentDir = path.join(root, "agent"); skill(agentDir, "conditional", "path: marker\n"); const env = { PI_CODING_AGENT_DIR: agentDir }; const baseline = await createTestSession({ cwd: root, env }); const without = baseline.session.systemPrompt; baseline.dispose(); t = await createTestSession({ cwd: root, extensions: [extension], env }); expect(t.session.systemPrompt).toBe(without); writeFileSync(path.join(root, "marker"), ""); await t.session.reload(); expect(t.session.systemPrompt.length).toBeGreaterThan(without.length); }); it("reports eligible skills at startup but only warnings on reload", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-notify-")); roots.push(root); const agentDir = path.join(root, "agent"); skill(agentDir, "invalid", "os: linux\n"); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_CODING_AGENT_DIR: agentDir }, }); skill(agentDir, "first", "os: [lnx, mac, win32]\n"); await t.session.bindExtensions({ mode: "tui" }); expect( t.events.uiCallsFor("notify").map((notice) => notice.args), ).toContainEqual(["skillz: eligible: first", "info"]); skill(agentDir, "second", "os: [lnx, mac, win32]\n"); t.events.ui.length = 0; await t.session.reload(); const notices = t.events.uiCallsFor("notify"); expect(notices.some((notice) => notice.args[1] === "info")).toBe(false); expect(notices.some((notice) => notice.args[1] === "warning")).toBe(true); expect( t.session.resourceLoader.getSkills().skills.map((skill) => skill.name), ).toContain("second"); }); it("/skillz uses discovery snapshot until reload without adding model context", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-inspect-")); roots.push(root); const agentDir = path.join(root, "agent"); skill( agentDir, "secret", "env: { all: [SKILLZ_TEST_TOKEN, SKILLZ_TEST_ENDPOINT] }\n", ); skill(agentDir, "excluded", "host: never-this-host\n"); skill(agentDir, "invalid", "env: invalid-name\n"); const secret = ["private-token", "DO-NOT-PRINT"].join("-"); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_CODING_AGENT_DIR: agentDir, SKILLZ_TEST_TOKEN: secret, SKILLZ_TEST_ENDPOINT: "", }, }); const before = t.session.systemPrompt; async function report(): Promise { const messages: string[] = []; const runner = t?.session.extensionRunner; const command = runner?.extensions[0].commands.get("skillz"); if (!command || !runner) throw new Error("missing skillz command"); const ctx = runner.createCommandContext(); Object.defineProperties(ctx, { mode: { value: "rpc" }, hasUI: { value: true }, ui: { value: { notify: (message: string) => messages.push(message) } }, }); await command.handler("", ctx); return messages.join("\n"); } const initial = await report(); expect(initial).toContain( "excluded: secret: env all (SKILLZ_TEST_TOKEN present, SKILLZ_TEST_ENDPOINT absent)", ); expect(initial).toContain("excluded: invalid: invalid env condition"); expect(initial).toContain("excluded: excluded: host (never-this-host ✗)"); expect(initial).not.toContain(secret); process.env.SKILLZ_TEST_ENDPOINT = "https://sensitive-endpoint.example"; expect(await report()).toBe(initial); await t.session.reload(); const updated = await report(); expect(updated).toContain( "loaded: secret: env all (SKILLZ_TEST_TOKEN present, SKILLZ_TEST_ENDPOINT present)", ); expect(updated).not.toContain(secret); expect(updated).not.toContain("https://sensitive-endpoint.example"); expect(t.session.systemPrompt).not.toBe(before); }); it("reevaluates markers after reload", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-reload-")); roots.push(root); const agentDir = path.join(root, "agent"); skill(agentDir, "conditional", "path: marker\n"); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_CODING_AGENT_DIR: agentDir }, }); expect( t.session.resourceLoader .getSkills() .skills.some((entry) => entry.name === "conditional"), ).toBe(false); writeFileSync(path.join(root, "marker"), ""); await t.session.reload(); expect( t.session.resourceLoader .getSkills() .skills.some((entry) => entry.name === "conditional"), ).toBe(true); rmSync(path.join(root, "marker")); await t.session.reload(); expect( t.session.resourceLoader .getSkills() .skills.some((entry) => entry.name === "conditional"), ).toBe(false); }); it("preserves native skill on name collision and follows linked skill files", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "skillz-collision-")); roots.push(root); const agentDir = path.join(root, "agent"); const native = path.join(root, "skills", "duplicate"); mkdirSync(native, { recursive: true }); writeFileSync( path.join(native, "SKILL.md"), "---\nname: duplicate\ndescription: Native skill\n---\n\n# Native\n", ); skill(agentDir, "duplicate", "os: [lnx, mac, win32]\n"); const outside = path.join(root, "outside"); mkdirSync(outside); writeFileSync( path.join(outside, "SKILL.md"), "---\nname: linked\ndescription: Linked skill\nos: [lnx, mac, win32]\n---\n\n# Linked\n", ); const linked = path.join(agentDir, "skillz", "linked"); mkdirSync(linked); symlinkSync(path.join(outside, "SKILL.md"), path.join(linked, "SKILL.md")); t = await createTestSession({ cwd: root, extensions: [extension], env: { PI_CODING_AGENT_DIR: agentDir }, }); const result = t.session.resourceLoader.getSkills(); const loaded = result.skills; expect(result.diagnostics.some((entry) => entry.type === "collision")).toBe( false, ); expect(loaded.filter((entry) => entry.name === "duplicate")).toHaveLength( 1, ); expect( loaded.find((entry) => entry.name === "duplicate")?.description, ).toBe("Native skill"); expect(loaded.some((entry) => entry.name === "linked")).toBe(true); }); });