import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { SKILL_MARKER_FILE, forgetSkillInstallRecords, installSkillToDirectories, installedSkillStatus, readSkillInstallRecord, readSkillMarker, uninstallRecordedSkills, uninstallSkillFromDirectory, writeSkillInstallRecord, writeSkillTo, } from "../out/agent/skill.js"; test("writeSkillTo creates SKILL.md and avoids project-doc noise", async () => { const dir = mkdtempSync(join(tmpdir(), "ra3-skill-test-")); try { const skillDir = join(dir, "ra3-mod-xml"); await writeSkillTo(skillDir, "0.1.25"); assert.ok(existsSync(join(skillDir, "SKILL.md"))); assert.ok(existsSync(join(skillDir, "references", "query-guide.md"))); assert.ok(existsSync(join(skillDir, SKILL_MARKER_FILE))); const content = readFileSync(join(skillDir, "SKILL.md"), "utf8"); assert.ok(content.includes("find_asset")); assert.ok(!content.includes("codebase-navigation-guide")); // The bundled reference must list every tool the MCP server exposes. const guide = readFileSync( join(skillDir, "references", "query-guide.md"), "utf8", ); for (const tool of [ "get_status", "find_asset", "find_references", "get_asset_references", "list_assets_by_type", "is_file_active", "find_define", "resolve_include", "list_projects", "get_usage_guide", ]) { assert.ok(guide.includes(tool), `query-guide.md must mention ${tool}`); } assert.equal(guide.includes("CnC3Types.xsd"), false); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("SKILL.md scopes itself to SAGE/RA3 projects and warns off others", async () => { const dir = mkdtempSync(join(tmpdir(), "ra3-skill-scope-")); try { const skillDir = join(dir, "ra3-mod-xml"); await writeSkillTo(skillDir, "0.1.25"); const content = readFileSync(join(skillDir, "SKILL.md"), "utf8"); // Must state its applicability and list concrete positive signals. assert.match(content, /When this skill applies/); assert.ok(content.includes("Data/Mod.xml")); assert.ok(content.includes("babproj")); assert.ok(content.includes("AssetDeclaration")); // Must give an explicit negative rule and a cheap probe. assert.match(content, /Do \*\*not\*\* use these tools for unrelated repositories/); assert.ok(content.includes("get_status")); assert.ok(content.includes("projectDir")); // `CnC3Types.xsd` is the shared SAGE base schema (the RA3 Mod SDK ships it // too), so it must not be presented as evidence in either direction. assert.equal(content.includes("CnC3Types.xsd"), false); assert.equal(content.includes("Tiberium Wars"), false); // Second-phase capability must be documented. assert.ok(content.includes("get_asset_references")); assert.ok(content.includes("definedIn")); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("SKILL.md explains how to reach the index without MCP", async () => { const dir = mkdtempSync(join(tmpdir(), "ra3-skill-reach-")); try { const skillDir = join(dir, "ra3-mod-xml"); await writeSkillTo(skillDir, "0.1.25"); const content = readFileSync(join(skillDir, "SKILL.md"), "utf8"); assert.match(content, /## Reaching the index/); // The discovery manifest is the stable entry point. assert.ok(content.includes("~/.ra3modxml/index.json")); // The launcher and the bundled CLI are both mentioned. assert.ok(content.includes("ra3-mod-xml-mcp")); assert.ok(content.includes("cli.js")); // The CLI is a Node script, and the skill must say how to run it when Node // is not installed (VS Code's Electron binary as Node). assert.ok(content.includes("ELECTRON_RUN_AS_NODE=1")); // The stdio escape hatch must be shown, since it needs no setup at all. assert.ok(content.includes("tools/call")); // It must not pretend configuring a client takes effect immediately. assert.ok(content.includes("new session")); // And it must refuse to invent results. assert.match(content, /Never fabricate index results/); // The bundled tool reference must be linked from SKILL.md, otherwise // skills-compatible clients never load it (resources load on demand). assert.ok(content.includes("./references/query-guide.md")); // The tool list must stay a real list; a merged bullet hides an item. assert.match(content, /indexed stream\.\n\s+- `find_define\(name\)`/); // Instructions must not send it looking for project-specific docs. assert.equal(content.includes("codebase-navigation-guide"), false); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("installSkillToDirectories records managed copies and uninstall removes them", async () => { const home = mkdtempSync(join(tmpdir(), "ra3-skill-home-")); const dir = mkdtempSync(join(tmpdir(), "ra3-skill-target-")); try { const target = join(dir, "ra3-mod-xml"); const succeeded = await installSkillToDirectories([target], "0.1.25", home); assert.deepEqual(succeeded, [target]); const record = await readSkillInstallRecord(home); assert.equal(record.length, 1); assert.equal(record[0].path, target); await uninstallSkillFromDirectory(target, home); assert.equal(existsSync(target), false); assert.equal((await readSkillInstallRecord(home)).length, 0); } finally { rmSync(home, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true }); } }); test("readSkillMarker requires our marker to describe the same directory", async () => { const dir = mkdtempSync(join(tmpdir(), "ra3-skill-marker-")); try { const managed = join(dir, "managed"); await writeSkillTo(managed, "1.0.0"); const marker = await readSkillMarker(managed); assert.equal(marker?.sourceVersion, "1.0.0"); // A hand-copied skill without a marker is never treated as ours. const foreign = join(dir, "foreign"); mkdirSync(foreign, { recursive: true }); writeFileSync(join(foreign, "SKILL.md"), "user content", "utf8"); assert.equal(await readSkillMarker(foreign), null); // A marker pointing at another directory must not make this one managed. const spoofed = join(dir, "spoofed"); mkdirSync(spoofed, { recursive: true }); writeFileSync( join(spoofed, SKILL_MARKER_FILE), JSON.stringify({ path: managed, sourceVersion: "1.0.0" }), "utf8", ); assert.equal(await readSkillMarker(spoofed), null); } finally { rmSync(dir, { recursive: true, force: true }); } }); test("uninstallRecordedSkills removes managed copies and skips user-owned ones", async () => { const home = mkdtempSync(join(tmpdir(), "ra3-skill-uninstall-home-")); const dir = mkdtempSync(join(tmpdir(), "ra3-skill-uninstall-")); try { const managed = join(dir, "managed"); await installSkillToDirectories([managed], "1.0.0", home); // A directory the user owns: recorded, but with no marker of ours. const foreign = join(dir, "foreign"); mkdirSync(foreign, { recursive: true }); writeFileSync(join(foreign, "SKILL.md"), "user content", "utf8"); const record = await readSkillInstallRecord(home); record.push({ path: foreign, sourceVersion: "0.0.0" }); await writeSkillInstallRecord(record, home); const status = await installedSkillStatus(home); assert.equal(status.length, 2); assert.deepEqual( status.map((s) => ({ path: s.path, managed: s.managed })), [ { path: managed, managed: true }, { path: foreign, managed: false }, ], ); const { removed, skipped } = await uninstallRecordedSkills( [managed, foreign], home, ); assert.deepEqual(removed, [managed]); assert.deepEqual(skipped, [foreign]); assert.equal(existsSync(managed), false); assert.equal(readFileSync(join(foreign, "SKILL.md"), "utf8"), "user content"); // The unmanaged entry stays in the record so it can still be reviewed. const after = await readSkillInstallRecord(home); assert.deepEqual(after.map((r) => r.path), [foreign]); } finally { rmSync(home, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true }); } }); test("installedSkillStatus and forgetSkillInstallRecords handle a missing directory", async () => { const home = mkdtempSync(join(tmpdir(), "ra3-skill-missing-home-")); const dir = mkdtempSync(join(tmpdir(), "ra3-skill-missing-")); try { const gone = join(dir, "gone"); await writeSkillInstallRecord([{ path: gone, sourceVersion: "1.0.0" }], home); const status = await installedSkillStatus(home); assert.equal(status.length, 1); assert.equal(status[0].exists, false); assert.equal(status[0].managed, false); await forgetSkillInstallRecords([gone], home); assert.equal((await readSkillInstallRecord(home)).length, 0); } finally { rmSync(home, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true }); } });