import assert from "node:assert/strict";
import test from "node:test";
import { mkdtempSync, rmSync, symlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import wrapper from "./wrapper.js";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const compatibilityMarker = "## Academic Research Skills compatibility for Pi";
const arsSkillNames = ["deep-research", "academic-paper", "academic-paper-reviewer", "academic-pipeline"];
const arsSkillLocations = arsSkillNames.map((name) => resolve(repoRoot, name, "SKILL.md"));
const externalSkillLocation = "/example/external/deep-research/SKILL.md";
const escapeXml = (text) => text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const skillBlock = (name, description, location) => `
${name}
${description}
${location}
`;
const baseSystemPrompt = [
"Base prompt",
"",
"",
...arsSkillNames.map((name, index) => skillBlock(name, "ARS skill", arsSkillLocations[index])),
skillBlock("deep-research", "Unrelated skill with a colliding name", externalSkillLocation),
"",
].join("\n");
function createHarness() {
const handlers = new Map();
const commands = new Map();
const entries = [];
const notifications = [];
let branch = [];
const sessionContext = {
sessionManager: { getBranch: () => branch },
};
const pi = {
on(name, handler) {
handlers.set(name, handler);
},
registerCommand(name, command) {
commands.set(name, command);
},
appendEntry(customType, data) {
const entry = { type: "custom", customType, data };
entries.push(entry);
branch.push(entry);
},
};
wrapper(pi);
handlers.get("session_start")({}, sessionContext);
return {
handlers,
commands,
entries,
notifications,
sessionContext,
setBranch(nextBranch) {
branch = nextBranch;
},
commandContext: {
ui: { notify: (message, level) => notifications.push({ message, level }) },
},
};
}
function runBeforeAgentStart(harness, systemPrompt = baseSystemPrompt) {
const result = harness.handlers.get("before_agent_start")({ systemPrompt });
return result?.systemPrompt ?? systemPrompt;
}
test("ordinary prompts hide only package ARS skills", () => {
const harness = createHarness();
const systemPrompt = runBeforeAgentStart(harness);
for (const location of arsSkillLocations) assert.equal(systemPrompt.includes(location), false);
assert.equal(systemPrompt.includes(externalSkillLocation), true);
assert.equal(systemPrompt.includes(compatibilityMarker), false);
});
test("skill hiding does not cross into an adjacent skill block", () => {
const systemPrompt = [
"Base prompt",
"",
"",
`
${arsSkillNames[0]}
ARS skill
${arsSkillLocations[0]}`,
skillBlock("deep-research", "Unrelated skill", externalSkillLocation),
"",
].join("\n");
const result = runBeforeAgentStart(createHarness(), systemPrompt);
assert.equal(result.includes(arsSkillLocations[0]), false);
assert.equal(result.includes(externalSkillLocation), true);
});
test("ordinary prompts hide ARS skills loaded through an XML-escaped symlink path", (t) => {
const tempRoot = mkdtempSync(join(tmpdir(), "ars-pi-&-"));
t.after(() => rmSync(tempRoot, { recursive: true, force: true }));
const linkedRoot = join(tempRoot, "linked-checkout");
symlinkSync(repoRoot, linkedRoot, process.platform === "win32" ? "junction" : "dir");
const linkedLocations = arsSkillNames.map((name) => resolve(linkedRoot, name, "SKILL.md"));
const systemPrompt = [
"Base prompt",
"",
"",
...arsSkillNames.map((name, index) => skillBlock(name, "ARS skill", escapeXml(linkedLocations[index]))),
skillBlock("deep-research", "Unrelated missing skill", "/missing/deep-research/SKILL.md"),
"",
].join("\n");
const result = runBeforeAgentStart(createHarness(), systemPrompt);
for (const location of linkedLocations) assert.equal(result.includes(escapeXml(location)), false);
assert.equal(result.includes("/missing/deep-research/SKILL.md"), true);
});
test("/ars-* activates compatibility for the same agent run", () => {
const harness = createHarness();
const result = harness.handlers.get("input")({ text: "/ars-plan topic" });
const systemPrompt = runBeforeAgentStart(harness);
assert.equal(result.action, "transform");
for (const location of arsSkillLocations) assert.equal(systemPrompt.includes(location), true);
assert.equal(systemPrompt.includes(compatibilityMarker), true);
});
test("direct ARS /skill:* activates compatibility", () => {
const harness = createHarness();
harness.handlers.get("input")({ text: "/skill:academic-pipeline topic" });
const systemPrompt = runBeforeAgentStart(harness);
assert.equal(systemPrompt.includes(compatibilityMarker), true);
});
test("command arguments are not rewritten as script paths", () => {
const harness = createHarness();
const result = harness.handlers.get("input")({
text: "/ars-cache-invalidate python scripts/user-provided.py",
});
assert.match(result.text, /python scripts\/user-provided\.py/);
assert.equal(result.text.includes(`${repoRoot}/scripts/user-provided.py`), false);
assert.equal(result.text.includes(`${repoRoot}/scripts/ars_cache_invalidate.py`), true);
});
test("argument placeholders are substituted in one pass", () => {
const harness = createHarness();
const result = harness.handlers.get("input")({
text: "/ars-cache-invalidate alpha$@omega",
});
assert.match(result.text, /alpha\$@omega/);
assert.doesNotMatch(result.text, /alphaalpha\$@omegaomega/);
});
test("/tree navigation recomputes activation from the selected branch", () => {
const harness = createHarness();
harness.handlers.get("input")({ text: "/ars-plan topic" });
assert.equal(runBeforeAgentStart(harness).includes(compatibilityMarker), true);
harness.setBranch([]);
const sessionTree = harness.handlers.get("session_tree");
assert.ok(sessionTree);
sessionTree({}, harness.sessionContext);
assert.equal(runBeforeAgentStart(harness).includes(compatibilityMarker), false);
harness.setBranch([{ type: "custom", customType: "ars-pi-state", data: { active: true } }]);
sessionTree({}, harness.sessionContext);
assert.equal(runBeforeAgentStart(harness).includes(compatibilityMarker), true);
});
test("/ars-pi-start and /ars-pi-stop toggle automatic invocation", async () => {
const harness = createHarness();
const start = harness.commands.get("ars-pi-start");
const stop = harness.commands.get("ars-pi-stop");
assert.ok(start);
await start.handler("", harness.commandContext);
assert.equal(runBeforeAgentStart(harness).includes(compatibilityMarker), true);
for (const location of arsSkillLocations) {
assert.equal(runBeforeAgentStart(harness).includes(location), true);
}
await stop.handler("", harness.commandContext);
assert.equal(runBeforeAgentStart(harness).includes(compatibilityMarker), false);
for (const location of arsSkillLocations) {
assert.equal(runBeforeAgentStart(harness).includes(location), false);
}
});
test("array prompts hide only ARS XML entries without flattening or mutating blocks", () => {
const prompt = Object.freeze(["Keep this first block, including its comma.", baseSystemPrompt, "Keep the final block."]);
const result = runBeforeAgentStart(createHarness(), prompt);
assert.equal(Array.isArray(result), true);
assert.equal(result[0], prompt[0]);
assert.equal(result[2], prompt[2]);
for (const location of arsSkillLocations) {
assert.equal(result[1].includes(location), false);
assert.equal(prompt[1].includes(location), true);
}
assert.equal(result[1].includes(externalSkillLocation), true);
assert.equal(result.join("\n").includes(compatibilityMarker), false);
});
test("array prompts append compatibility as a separate block and stop cleanly", async () => {
const harness = createHarness();
const prompt = Object.freeze(["First block, with a comma.", "Second block\nwith a newline."]);
harness.handlers.get("input")({ text: "/skill:academic-pipeline topic" });
const active = runBeforeAgentStart(harness, prompt);
assert.equal(Array.isArray(active), true);
assert.deepEqual(active.slice(0, -1), prompt);
assert.equal(active.at(-1).includes(compatibilityMarker), true);
assert.deepEqual(runBeforeAgentStart(harness, prompt), active);
await harness.commands.get("ars-pi-stop").handler("", harness.commandContext);
assert.deepEqual(runBeforeAgentStart(harness, prompt), prompt);
});
function structuredOptions() {
return {
skills: [
...arsSkillNames.map((name, index) => ({ name, filePath: arsSkillLocations[index] })),
{ name: "deep-research", filePath: externalSkillLocation },
],
sections: { mcp_servers: "Keep the MCP server instructions." },
};
}
function runStructuredBeforeAgentStart(harness, options) {
const result = harness.handlers.get("before_agent_start")({
systemPromptOptions: options,
get systemPrompt() {
assert.fail("Structured handling must not read or replace a rendered prompt snapshot");
},
});
assert.equal(result, undefined);
}
test("structured inactive prompts filter package skills and preserve extension sections", () => {
const options = structuredOptions();
const originalSkills = Object.freeze(options.skills);
const sections = options.sections;
runStructuredBeforeAgentStart(createHarness(), options);
assert.deepEqual(options.skills, [{ name: "deep-research", filePath: externalSkillLocation }]);
assert.equal(originalSkills.length, 5);
assert.equal(options.sections, sections);
assert.equal(options.sections.mcp_servers, "Keep the MCP server instructions.");
assert.equal(options.sections.ars_compatibility, undefined);
});
test("structured active prompts add one compatibility section without replacing skills or MCP", () => {
const harness = createHarness();
harness.handlers.get("input")({ text: "/ars-plan topic" });
const options = structuredOptions();
const skills = options.skills;
const sections = options.sections;
runStructuredBeforeAgentStart(harness, options);
const compatibility = options.sections.ars_compatibility;
runStructuredBeforeAgentStart(harness, options);
assert.equal(options.skills, skills);
assert.equal(options.sections, sections);
assert.equal(options.sections.mcp_servers, "Keep the MCP server instructions.");
assert.ok(compatibility.includes(compatibilityMarker));
assert.equal(options.sections.ars_compatibility, compatibility);
assert.equal(compatibility.split(compatibilityMarker).length - 1, 1);
});
test("later extensions can add sections after either structured ARS branch", () => {
for (const active of [false, true]) {
const harness = createHarness();
if (active) harness.handlers.get("input")({ text: "/skill:academic-pipeline topic" });
const options = structuredOptions();
runStructuredBeforeAgentStart(harness, options);
options.sections.later_extension = "Added after ARS.";
assert.equal(options.sections.later_extension, "Added after ARS.");
assert.equal(options.sections.mcp_servers, "Keep the MCP server instructions.");
assert.equal(Object.hasOwn(options, "forceSystemPrompt"), false);
}
});
test("structured inactive filtering resolves symlinks and keeps missing or colliding skills", (t) => {
const tempRoot = mkdtempSync(join(tmpdir(), "ars-pi-structured-&-"));
t.after(() => rmSync(tempRoot, { recursive: true, force: true }));
const linkedRoot = join(tempRoot, "linked-checkout");
symlinkSync(repoRoot, linkedRoot, process.platform === "win32" ? "junction" : "dir");
const options = structuredOptions();
options.skills = [
...arsSkillNames.map((name) => ({ name, filePath: resolve(linkedRoot, name, "SKILL.md") })),
{ name: "deep-research", filePath: externalSkillLocation },
{ name: "academic-paper", filePath: "/missing/academic-paper/SKILL.md" },
];
runStructuredBeforeAgentStart(createHarness(), options);
assert.deepEqual(options.skills.map((skill) => skill.filePath), [
externalSkillLocation, "/missing/academic-paper/SKILL.md",
]);
});
test("stopping ARS removes its structured section but retains other extensions", async () => {
const harness = createHarness();
await harness.commands.get("ars-pi-start").handler("", harness.commandContext);
const options = structuredOptions();
runStructuredBeforeAgentStart(harness, options);
assert.ok(options.sections.ars_compatibility.includes(compatibilityMarker));
await harness.commands.get("ars-pi-stop").handler("", harness.commandContext);
runStructuredBeforeAgentStart(harness, options);
assert.equal(options.sections.ars_compatibility, undefined);
assert.equal(options.sections.mcp_servers, "Keep the MCP server instructions.");
assert.deepEqual(options.skills, [{ name: "deep-research", filePath: externalSkillLocation }]);
});
for (const active of [false, true]) {
for (const array of [false, true]) {
test(`legacy options without sections use rendered fallback (active=${active}, array=${array})`, () => {
const harness = createHarness();
if (active) harness.handlers.get("input")({ text: "/ars-plan topic" });
const options = structuredOptions();
delete options.sections;
const skills = options.skills;
const prompt = array ? Object.freeze(["First block", baseSystemPrompt]) : baseSystemPrompt;
const { systemPrompt } = harness.handlers.get("before_agent_start")({
systemPrompt: prompt, systemPromptOptions: options,
});
assert.equal(Array.isArray(systemPrompt), array);
const rendered = array ? systemPrompt.join("\n") : systemPrompt;
for (const location of arsSkillLocations) assert.equal(rendered.includes(location), active);
assert.equal(rendered.includes(externalSkillLocation), true);
assert.equal(rendered.includes(compatibilityMarker), active);
assert.equal(options.skills, skills);
assert.equal(options.sections, undefined);
});
}
test(`earlier complete-prompt override uses rendered fallback (active=${active})`, () => {
const harness = createHarness();
if (active) harness.handlers.get("input")({ text: "/ars-plan topic" });
for (const forceSystemPrompt of [`${baseSystemPrompt}\nEarlier extension sentinel`, ""]) {
const options = { ...structuredOptions(), forceSystemPrompt };
const skills = options.skills;
const { systemPrompt } = harness.handlers.get("before_agent_start")({
systemPrompt: forceSystemPrompt, systemPromptOptions: options,
});
assert.equal(systemPrompt.includes(compatibilityMarker), active);
if (forceSystemPrompt) {
assert.equal(systemPrompt.includes("Earlier extension sentinel"), true);
for (const location of arsSkillLocations) assert.equal(systemPrompt.includes(location), active);
assert.equal(systemPrompt.includes(externalSkillLocation), true);
}
assert.equal(options.skills, skills);
assert.equal(options.sections.ars_compatibility, undefined);
assert.equal(options.forceSystemPrompt, forceSystemPrompt);
}
});
}