import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdirSync, writeFileSync, rmSync } from "node:fs"; import { dirname, join } from "node:path"; import { expandPromptTemplateFromDisk } from "../prompt-expander.js"; import { parseSkillBlock } from "@blackbelt-technology/pi-dashboard-shared/skill-block-parser.js"; const tmpDir = join(import.meta.dirname ?? __dirname, "__tmp_prompt_test__"); const promptsDir = join(tmpDir, ".pi", "prompts"); const skillsDir = join(tmpDir, ".pi", "skills"); beforeEach(() => { mkdirSync(promptsDir, { recursive: true }); writeFileSync(join(promptsDir, "opsx-continue.md"), "---\ndescription: continue\n---\nContinue the change"); writeFileSync(join(promptsDir, "opsx-apply.md"), "Apply the change"); writeFileSync(join(promptsDir, "hello.md"), "Hello world"); // Skill fixture mkdirSync(join(skillsDir, "my-skill"), { recursive: true }); writeFileSync( join(skillsDir, "my-skill", "SKILL.md"), "---\nname: my-skill\ndescription: A demo skill\n---\nFirst body line\nSecond body line", ); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); describe("expandPromptTemplateFromDisk", () => { it("expands hyphen form /opsx-continue", () => { const result = expandPromptTemplateFromDisk("/opsx-continue my-change", tmpDir); expect(result).toContain("Continue the change"); expect(result).toContain("my-change"); }); it("expands colon form /opsx:continue as alias for /opsx-continue", () => { const result = expandPromptTemplateFromDisk("/opsx:continue my-change", tmpDir); expect(result).toContain("Continue the change"); expect(result).toContain("my-change"); }); it("expands colon form /opsx:apply without args", () => { const result = expandPromptTemplateFromDisk("/opsx:apply", tmpDir); expect(result).toBe("Apply the change"); }); it("does not affect non-opsx colon commands", () => { // /hello has no colon, should work as before const result = expandPromptTemplateFromDisk("/hello", tmpDir); expect(result).toBe("Hello world"); }); it("returns original text when no template found", () => { const result = expandPromptTemplateFromDisk("/nonexistent", tmpDir); expect(result).toBe("/nonexistent"); }); it("strips YAML frontmatter from colon form too", () => { const result = expandPromptTemplateFromDisk("/opsx:continue", tmpDir); expect(result).toBe("Continue the change"); expect(result).not.toContain("---"); }); // See change: render-skill-invocations-collapsibly. it("wraps /skill:my-skill output in a envelope (with args)", () => { const result = expandPromptTemplateFromDisk("/skill:my-skill do the thing", tmpDir); expect(result.startsWith(' envelope (without args)", () => { const result = expandPromptTemplateFromDisk("/skill:my-skill", tmpDir); expect(result.startsWith('")).toBe(true); expect(result).not.toContain("\n\n"); const parsed = parseSkillBlock(result); expect(parsed!.args).toBeUndefined(); expect(parsed!.condensed).toBe("/skill:my-skill"); }); it("prompt template /opsx-continue stays unwrapped (no tag)", () => { const result = expandPromptTemplateFromDisk("/opsx-continue my-change", tmpDir); expect(result).not.toContain(""); }); it("colon-alias prompt template /opsx:continue stays unwrapped", () => { const result = expandPromptTemplateFromDisk("/opsx:continue x", tmpDir); expect(result).not.toContain(" { const skillPath = makeSkillFile("registry/colon/SKILL.md"); const pi = { getCommands: () => [{ name: "opsx:archive", source: "skill", path: skillPath }], }; const result = expandPromptTemplateFromDisk("/opsx-archive my-change", tmpDir, pi); expect(result.startsWith(' { const skillPath = makeSkillFile("registry/hyphen/SKILL.md"); const pi = { getCommands: () => [{ name: "opsx-archive", source: "skill", path: skillPath }], }; const result = expandPromptTemplateFromDisk("/opsx:archive my-change", tmpDir, pi); expect(result.startsWith(' { mkdirSync(join(skillsDir, "opsx-archive"), { recursive: true }); writeFileSync(join(skillsDir, "opsx-archive", "SKILL.md"), "---\nname: x\n---\nbody"); const result = expandPromptTemplateFromDisk("/opsx:archive arg", tmpDir); expect(result.startsWith(' { mkdirSync(join(skillsDir, "opsx:archive"), { recursive: true }); writeFileSync(join(skillsDir, "opsx:archive", "SKILL.md"), "---\nname: x\n---\nbody"); const result = expandPromptTemplateFromDisk("/opsx-archive arg", tmpDir); expect(result.startsWith(' { // Local prompt opsx-foo.md exists; registry has skill opsx:foo. writeFileSync(join(promptsDir, "opsx-foo.md"), "prompt body"); const skillPath = makeSkillFile("registry/precedence/SKILL.md", "skill body"); const pi = { getCommands: () => [{ name: "opsx:foo", source: "skill", path: skillPath }], }; // /opsx:foo → must wrap as skill (registry hit on original form). const colon = expandPromptTemplateFromDisk("/opsx:foo", tmpDir, pi); expect(colon.startsWith(' { const aPath = makeSkillFile("registry/A/SKILL.md", "A body"); const bPath = makeSkillFile("registry/B/SKILL.md", "B body"); const pi = { getCommands: () => [ { name: "opsx:foo", source: "skill", path: aPath }, { name: "opsx-foo", source: "skill", path: bPath }, ], }; const colon = expandPromptTemplateFromDisk("/opsx:foo arg", tmpDir, pi); expect(colon).toContain(`location="${aPath}"`); expect(colon).toContain('name="opsx:foo"'); expect(colon).not.toContain(`location="${bPath}"`); const hyphen = expandPromptTemplateFromDisk("/opsx-foo arg", tmpDir, pi); expect(hyphen).toContain(`location="${bPath}"`); expect(hyphen).toContain('name="opsx-foo"'); expect(hyphen).not.toContain(`location="${aPath}"`); }); it("original form in pi-registry beats remapped form in local-scan", () => { // Local prompt opsx-foo.md exists; registry has skill opsx:foo. writeFileSync(join(promptsDir, "opsx-foo.md"), "prompt body"); const skillPath = makeSkillFile("registry/outer/SKILL.md", "skill body"); const pi = { getCommands: () => [{ name: "opsx:foo", source: "skill", path: skillPath }], }; // /opsx:foo: outer-loop probes original form across ALL stores first. // Step 3 hit on registry — must NOT fall through to remapped opsx-foo local prompt. const result = expandPromptTemplateFromDisk("/opsx:foo", tmpDir, pi); expect(result.startsWith(' { rmSync(tmpDir, { recursive: true, force: true }); mkdirSync(tmpDir, { recursive: true }); const result = expandPromptTemplateFromDisk("/opsx:nonexistent foo", tmpDir); expect(result).toBe("/opsx:nonexistent foo"); }); });