import assert from "node:assert/strict"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; import { __testing } from "../extensions/skill-registry.ts"; test("project skill dirs include supported workspace roots", () => { const cwd = "/repo"; const dirs = __testing.projectSkillDirs(cwd); for (const want of [ "skills", ".opencode/skills", ".claude/skills", ".gemini/skills", ".cursor/skills", ".github/skills", ".codex/skills", ".qwen/skills", ".kiro/skills", ".openclaw/skills", ".pi/skills", ".agent/skills", ".agents/skills", ".atl/skills", ]) { assert.ok(dirs.includes(join(cwd, want)), `missing ${want}`); } }); test("registry renders indexed skill paths instead of compact rules", () => { const cwd = join(tmpdir(), `gentle-pi-render-${Date.now()}`); const skillPath = join(cwd, "skills", "go-testing", "SKILL.md"); const registry = __testing.renderRegistry(cwd, ["skills"], [ { name: "go-testing", path: skillPath, description: "Trigger: Go tests. Apply focused testing patterns.", }, ]); assert.match(registry, /## Skills/); assert.match(registry, /\| Skill \| Trigger \/ description \| Scope \| Path \|/); assert.match(registry, /## Loading protocol/); assert.match(registry, /\| `go-testing` \| Trigger: Go tests\. Apply focused testing patterns\. \| project \|/); assert.match(registry, new RegExp(skillPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.doesNotMatch(registry, /Selected skills and compact rules/); assert.doesNotMatch(registry, /Project Standards \(auto-resolved\)/); assert.doesNotMatch(registry, /Rules:/); }); test("frontmatter parser keeps full multiline descriptions", () => { const parsed = __testing.parseFrontmatter(`--- name: ai-sdk-5 description: > Trigger: AI chat features, Vercel AI SDK 5, streaming UI. Use AI SDK 5 patterns and avoid v4 APIs. license: Apache-2.0 --- ## Hard Rules - Do not copy this rule. `); assert.equal(parsed.name, "ai-sdk-5"); assert.equal( parsed.description, "Trigger: AI chat features, Vercel AI SDK 5, streaming UI. Use AI SDK 5 patterns and avoid v4 APIs.", ); }); test("description normalization preserves trigger and collapses whitespace", () => { assert.equal( __testing.normalizeSkillDescription("Trigger: PR feedback, issue replies.\nUse maintainer voice."), "Trigger: PR feedback, issue replies. Use maintainer voice.", ); }); test("project-scoped duplicate wins over user duplicate", () => { const cwd = join(tmpdir(), `gentle-pi-registry-${Date.now()}`); const projectPath = join(cwd, ".opencode/skills/dup/SKILL.md"); const userPath = join(cwd + "-home", ".config/opencode/skills/dup/SKILL.md"); const entries = [ { name: "dup", path: userPath, description: "user" }, { name: "dup", path: projectPath, description: "project" }, ]; const [chosen] = __testing.dedupeBySkillName(entries, cwd); assert.equal(chosen.path, projectPath); }); test("uniqueExistingDirs normalizes duplicates and ignores missing roots", async () => { const root = join(tmpdir(), `gentle-pi-existing-${Date.now()}`); const existing = join(root, "skills"); mkdirSync(existing, { recursive: true }); assert.deepEqual( await __testing.uniqueExistingDirs([existing, join(root, "skills/"), join(root, "missing")]), [existing], ); }); test("startup skip honors no skill registry controls", () => { const enabled = { getFlag: () => true }; const disabled = { getFlag: () => false }; assert.equal(__testing.shouldSkipSkillRegistryStartup(enabled, [], {}), true); assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["--no-skills"], {}), true); assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["-ns"], {}), true); assert.equal( __testing.shouldSkipSkillRegistryStartup(disabled, [], { GENTLE_PI_NO_SKILL_REGISTRY: "1" }), true, ); assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, [], {}), false); }); test("scope and markdown cells are represented in registry", () => { const cwd = join(tmpdir(), `gentle-pi-scope-${Date.now()}`); const projectPath = join(cwd, "skills", "docs", "SKILL.md"); const userPath = join(tmpdir(), `gentle-pi-home-${Date.now()}`, ".claude", "skills", "docs", "SKILL.md"); const registry = __testing.renderRegistry(cwd, ["skills"], [ { name: "project-docs", path: projectPath, description: "Docs | guides" }, { name: "user-docs", path: userPath, description: "" }, ]); assert.match(registry, /\| `project-docs` \| Docs \\\| guides \| project \|/); assert.match(registry, /\| `user-docs` \| — \| user \|/); }); test("generated registry file indexes skill path and omits body rules", async () => { const cwd = join(tmpdir(), `gentle-pi-regenerate-${Date.now()}`); const skillPath = join(cwd, "skills", "go-testing", "SKILL.md"); mkdirSync(dirname(skillPath), { recursive: true }); writeFileSync( skillPath, `--- name: go-testing description: "Trigger: Go tests. Apply focused Go testing patterns." --- ## Hard Rules - Run focused tests before broad tests. `, ); const dirs = await __testing.uniqueExistingDirs(__testing.projectSkillDirs(cwd)); assert.ok(dirs.includes(join(cwd, "skills"))); const registry = __testing.renderRegistry(cwd, ["skills"], [ { name: "go-testing", path: skillPath, description: "Trigger: Go tests. Apply focused Go testing patterns.", }, ]); assert.match(registry, /go-testing/); assert.match(registry, /Trigger: Go tests\. Apply focused Go testing patterns\./); assert.match(registry, new RegExp(skillPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.doesNotMatch(registry, /Run focused tests before broad tests/); }); test("orchestrator documents path injection protocol", () => { const source = readFileSync(join(import.meta.dirname, "..", "assets", "orchestrator.md"), "utf8"); assert.match(source, /## Skills to load before work/); assert.match(source, /paths-injected/); assert.doesNotMatch(source, /Use matching compact rules based on code context and task intent/); });