import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { afterEach, beforeEach, describe, it } from "node:test";
import {
buildSkillInjection,
clearSkillCache,
discoverAvailableSkills,
resolveSkills,
resolveSkillsWithFallback,
} from "../../src/agents/skills.ts";
let tempDir = "";
function writeSkillFile(skillDir: string, body: string, description = "Test description"): void {
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, "SKILL.md"),
`---\ndescription: ${description}\n---\n\n${body}\n`,
"utf-8",
);
}
function makeProjectSkill(cwd: string, name: string, body: string, description = "Test description"): void {
const skillDir = path.join(cwd, ".selesai", "skills", name);
writeSkillFile(skillDir, body, description);
}
function makeProjectPackageSkill(cwd: string, packageName: string, name: string, body: string): void {
const packageRoot = path.join(cwd, ".selesai", "npm", "node_modules", packageName);
makePackageSkill(packageRoot, name, body, packageName);
}
function makePackageSkill(packageRoot: string, name: string, body: string, packageName = `${name}-pkg`): void {
const skillDir = path.join(packageRoot, "skills", name);
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: packageName, version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
"utf-8",
);
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `${body}\n`, "utf-8");
}
async function importSkillsFresh() {
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const modulePath = path.resolve(projectRoot, "src/agents/skills.ts");
const bust = `${Date.now()}-${Math.random()}`;
return await import(`${pathToFileURL(modulePath).href}?bust=${bust}`) as typeof import("../../src/agents/skills.ts");
}
async function importAgentsFresh() {
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const modulePath = path.resolve(projectRoot, "src/agents/agents.ts");
const bust = `${Date.now()}-${Math.random()}`;
return await import(`${pathToFileURL(modulePath).href}?bust=${bust}`) as typeof import("../../src/agents/agents.ts");
}
describe("skills filesystem fallback", () => {
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-skills-fallback-"));
clearSkillCache();
});
afterEach(() => {
clearSkillCache();
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("discovers project skills from filesystem paths", () => {
makeProjectSkill(tempDir, "fallback-skill", "Use fallback mode.");
const skills = discoverAvailableSkills(tempDir);
const discovered = skills.find((skill) => skill.name === "fallback-skill");
assert.ok(discovered, "expected fallback-skill to be discovered");
assert.equal(discovered?.source, "project");
assert.equal(discovered?.description, "Test description");
});
it("reads block scalar descriptions for discovery and child injection", () => {
const cases = [
["|", "first line\nsecond line"],
["|-", "first line\nsecond line"],
[">", "first line second line"],
[">-", "first line second line"],
] as const;
for (const [index, [indicator]] of cases.entries()) {
makeProjectSkill(tempDir, `block-scalar-${index}`, "Use block scalar metadata.", `${indicator}\n first line\n second line`);
}
for (const [index, [, description]] of cases.entries()) {
const name = `block-scalar-${index}`;
const discovered = discoverAvailableSkills(tempDir).find((skill) => skill.name === name);
assert.equal(discovered?.description, description);
const { resolved, missing } = resolveSkills([name], tempDir);
assert.deepEqual(missing, []);
assert.match(buildSkillInjection(resolved), new RegExp(`${description.replace("\n", "\\n")}`));
}
});
it("discovers project skills nested below grouping directories", () => {
writeSkillFile(
path.join(tempDir, ".selesai", "skills", "shell", "issue-262-nested-skill"),
"Use nested project skill.",
"Nested issue 262 skill",
);
const skills = discoverAvailableSkills(tempDir);
const discovered = skills.find((skill) => skill.name === "issue-262-nested-skill");
assert.ok(discovered, "expected grouped nested skill to be discovered");
assert.equal(discovered?.source, "project");
assert.equal(discovered?.description, "Nested issue 262 skill");
const { resolved, missing } = resolveSkills(["issue-262-nested-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.match(resolved[0]?.content ?? "", /Use nested project skill\./);
});
it("stops recursive project skill discovery at the first SKILL.md anchor", () => {
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
writeSkillFile(path.join(groupedRoot, "issue-262-anchor"), "Use anchor skill.");
writeSkillFile(path.join(groupedRoot, "issue-262-anchor", "nested", "issue-262-leaked-skill"), "Should not leak.");
writeSkillFile(path.join(groupedRoot, "issue-262-sibling"), "Use sibling skill.");
const names = discoverAvailableSkills(tempDir).map((skill) => skill.name);
assert.equal(names.includes("issue-262-anchor"), true);
assert.equal(names.includes("issue-262-sibling"), true);
assert.equal(names.includes("issue-262-leaked-skill"), false);
});
it("skips hidden directories and node_modules while recursing for project skills", () => {
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
writeSkillFile(path.join(groupedRoot, ".hidden", "issue-262-hidden-skill"), "Should stay hidden.");
writeSkillFile(path.join(groupedRoot, "node_modules", "issue-262-node-skill"), "Should stay ignored.");
writeSkillFile(path.join(groupedRoot, "visible", "issue-262-visible-skill"), "Use visible nested skill.");
const names = discoverAvailableSkills(tempDir).map((skill) => skill.name);
assert.equal(names.includes("issue-262-visible-skill"), true);
assert.equal(names.includes("issue-262-hidden-skill"), false);
assert.equal(names.includes("issue-262-node-skill"), false);
});
it("keeps direct markdown skills from explicit settings roots after parent recursion", () => {
const groupedRoot = path.join(tempDir, ".selesai", "skills", "group");
fs.mkdirSync(groupedRoot, { recursive: true });
fs.writeFileSync(path.join(groupedRoot, "issue-262-direct.md"), "Use direct markdown skill.\n", "utf-8");
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ skills: ["./skills/group"] }, null, 2),
"utf-8",
);
const { resolved, missing } = resolveSkills(["issue-262-direct"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-settings");
assert.match(resolved[0]?.content ?? "", /Use direct markdown skill\./);
});
it("keeps nested skills from higher-priority explicit settings roots after parent recursion", () => {
writeSkillFile(
path.join(tempDir, "skills", "group", "issue-262-settings-nested"),
"Use settings nested skill.",
);
fs.writeFileSync(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "fixture", version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
"utf-8",
);
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ skills: ["../skills/group"] }, null, 2),
"utf-8",
);
const { resolved, missing } = resolveSkills(["issue-262-settings-nested"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-settings");
assert.match(resolved[0]?.content ?? "", /Use settings nested skill\./);
});
it("keeps nested skills from higher-priority explicit settings roots when the root path is duplicated", () => {
writeSkillFile(
path.join(tempDir, "skills", "group", "issue-262-settings-same-root"),
"Use settings same root skill.",
);
fs.writeFileSync(
path.join(tempDir, "package.json"),
JSON.stringify({ name: "fixture", version: "1.0.0", pi: { skills: ["./skills"] } }, null, 2),
"utf-8",
);
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ skills: ["../skills"] }, null, 2),
"utf-8",
);
const { resolved, missing } = resolveSkills(["issue-262-settings-same-root"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-settings");
assert.match(resolved[0]?.content ?? "", /Use settings same root skill\./);
});
it("resolves and reads skill content via filesystem fallback", () => {
makeProjectSkill(tempDir, "resolve-skill", "Run local fallback checks.");
const { resolved, missing } = resolveSkills(["resolve-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.name, "resolve-skill");
assert.equal(resolved[0]?.source, "project");
assert.match(resolved[0]?.content ?? "", /Run local fallback checks\./);
});
it("builds lazy skill references instead of inlining full skill bodies", () => {
makeProjectSkill(tempDir, "lazy-skill", "This body should stay out of the system prompt.");
const { resolved, missing } = resolveSkills(["lazy-skill"], tempDir);
assert.deepEqual(missing, []);
const injection = buildSkillInjection(resolved);
assert.match(injection, /The following configured skills are available to this subagent/);
assert.match(injection, /Use the read tool to load a skill's file/);
assert.match(injection, //);
assert.match(injection, /lazy-skill<\/name>/);
assert.match(injection, /Test description<\/description>/);
assert.match(injection, /.*lazy-skill.*SKILL\.md<\/location>/);
assert.doesNotMatch(injection, /This body should stay out/);
assert.doesNotMatch(injection, / {
makeProjectSkill(tempDir, "amp&skill", "Body", "Use A & B ");
const { resolved } = resolveSkills(["amp&skill"], tempDir);
const injection = buildSkillInjection(resolved);
assert.match(injection, /amp&skill<\/name>/);
assert.match(injection, /Use A & B <carefully><\/description>/);
assert.match(injection, /amp&skill[\\/]SKILL\.md/);
});
it("does not expose pi-subagents as a child-injectable skill", () => {
makeProjectSkill(tempDir, "pi-subagents", "Parent orchestration only.");
makeProjectSkill(tempDir, "safe-bash", "Use safe bash.");
const available = discoverAvailableSkills(tempDir).map((skill) => skill.name);
assert.equal(available.includes("pi-subagents"), false);
assert.equal(available.includes("safe-bash"), true);
const { resolved, missing } = resolveSkills(["pi-subagents", "safe-bash"], tempDir);
assert.deepEqual(missing, ["pi-subagents"]);
assert.deepEqual(resolved.map((skill) => skill.name), ["safe-bash"]);
const agentDir = path.join(tempDir, "agent");
writeSkillFile(path.join(agentDir, "skills", "pi-subagents"), "Still parent-only.");
const local = resolveSkills(["pi-subagents"], tempDir, ["./skills"], agentDir);
assert.deepEqual(local.resolved, []);
assert.deepEqual(local.missing, ["pi-subagents"]);
});
it("classifies package-provided skills as project-package", () => {
makeProjectPackageSkill(tempDir, "test-skill-package", "pkg-skill", "Use package skill.");
const skills = discoverAvailableSkills(tempDir);
const discovered = skills.find((skill) => skill.name === "pkg-skill");
assert.ok(discovered, "expected pkg-skill to be discovered");
assert.equal(discovered?.source, "project-package");
});
it("prefers project skills over project-package skills with the same name", () => {
makeProjectPackageSkill(tempDir, "test-skill-package", "shared-skill", "Package version");
makeProjectSkill(tempDir, "shared-skill", "Project version");
const { resolved, missing } = resolveSkills(["shared-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project");
assert.match(resolved[0]?.content ?? "", /Project version/);
});
it("discovers skills from project settings packages", () => {
const packageRoot = path.join(tempDir, ".selesai", "packages", "local-skill-pkg");
makePackageSkill(packageRoot, "settings-package-skill", "Settings package skill.");
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ packages: ["./packages/local-skill-pkg"] }, null, 2),
"utf-8",
);
const { resolved, missing } = resolveSkills(["settings-package-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-package");
});
it("discovers skills from project settings npm package sources", () => {
const packageRoot = path.join(tempDir, ".selesai", "npm", "node_modules", "@scope", "skill-package");
makePackageSkill(
packageRoot,
"project-settings-scoped-npm-package-skill",
"Project settings scoped npm package skill.",
"@scope/skill-package",
);
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ packages: ["npm:@scope/skill-package@1.2.3"] }, null, 2),
"utf-8",
);
const { resolved, missing } = resolveSkills(["project-settings-scoped-npm-package-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-package");
});
it("discovers skills from the current cwd package", () => {
makePackageSkill(tempDir, "cwd-package-skill", "Cwd package skill.");
const { resolved, missing } = resolveSkills(["cwd-package-skill"], tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-package");
});
it("skips optional global npm discovery in offline mode", () => {
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const binDir = path.join(tempDir, "bin");
const fakeHome = path.join(tempDir, "home");
const marker = path.join(tempDir, "npm-calls.txt");
fs.mkdirSync(binDir, { recursive: true });
fs.mkdirSync(fakeHome, { recursive: true });
fs.writeFileSync(
path.join(binDir, "npm"),
"#!/bin/sh\nprintf 'npm-root-called\\n' >> \"$PI_DISCOVERY_MARKER\"\nexit 1\n",
{ encoding: "utf-8", mode: 0o755 },
);
fs.writeFileSync(
path.join(binDir, "npm.cmd"),
"@echo off\r\n>>\"%PI_DISCOVERY_MARKER%\" echo npm-root-called\r\nexit /b 1\r\n",
"utf-8",
);
const script = `
import fs from "node:fs";
const [{ clearSkillCache, discoverAvailableSkills }, { discoverAgents }] = await Promise.all([
import("./src/agents/skills.ts"),
import("./src/agents/agents.ts"),
]);
discoverAvailableSkills(process.cwd());
discoverAgents(process.cwd(), "both");
if (fs.existsSync(process.env.PI_DISCOVERY_MARKER)) {
throw new Error("npm was invoked while PI_OFFLINE was enabled");
}
delete process.env.PI_OFFLINE;
clearSkillCache();
discoverAvailableSkills(process.cwd());
discoverAgents(process.cwd(), "both");
`;
execFileSync(
process.execPath,
["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script],
{
cwd: projectRoot,
env: {
...process.env,
HOME: fakeHome,
USERPROFILE: fakeHome,
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
PI_DISCOVERY_MARKER: marker,
PI_OFFLINE: "1",
},
stdio: "pipe",
},
);
assert.deepEqual(fs.readFileSync(marker, "utf-8").trim().split(/\r?\n/), ["npm-root-called", "npm-root-called"]);
});
it("uses the Windows APPDATA npm root without invoking npm", async () => {
const appData = path.join(tempDir, "appdata");
const packageRoot = path.join(appData, "npm", "node_modules", "windows-global-package");
const marker = path.join(tempDir, "npm-called");
const binDir = path.join(tempDir, "bin");
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(
path.join(binDir, "npm"),
`#!/bin/sh\ntouch "${marker}"\nexit 1\n`,
{ encoding: "utf-8", mode: 0o755 },
);
writeSkillFile(path.join(packageRoot, "skills", "windows-global-skill"), "Use the Windows global skill.");
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({
name: "windows-global-package",
pi: { skills: ["./skills"] },
"pi-subagents": { agents: ["./agents"] },
}, null, 2),
"utf-8",
);
fs.mkdirSync(path.join(packageRoot, "agents"), { recursive: true });
fs.writeFileSync(path.join(packageRoot, "agents", "windows-global-agent.md"), `---
name: windows-global-agent
description: Loaded from the Windows global npm root.
---
Windows global agent.
`, "utf-8");
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
const previousAppData = process.env.APPDATA;
const previousPath = process.env.PATH;
try {
Object.defineProperty(process, "platform", { value: "win32" });
process.env.APPDATA = appData;
process.env.PATH = `${binDir}${path.delimiter}${previousPath ?? ""}`;
const skills = await importSkillsFresh();
assert.ok(skills.discoverAvailableSkills(tempDir).some((skill) => skill.name === "windows-global-skill"));
const agents = await importAgentsFresh();
assert.ok(agents.discoverAgents(tempDir, "both").agents.some((agent) => agent.name === "windows-global-agent"));
assert.equal(fs.existsSync(marker), false, "npm root -g should not run when APPDATA has a global root");
} finally {
if (platformDescriptor) Object.defineProperty(process, "platform", platformDescriptor);
if (previousAppData === undefined) delete process.env.APPDATA;
else process.env.APPDATA = previousAppData;
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
}
});
it("falls back to npm when the Windows APPDATA npm root is invalid", async () => {
const appData = path.join(tempDir, "appdata-file");
const fallbackRoot = path.join(tempDir, "fallback-global-root");
const packageRoot = path.join(fallbackRoot, "fallback-global-package");
const marker = path.join(tempDir, "npm-called");
const binDir = path.join(tempDir, "bin");
fs.mkdirSync(path.join(appData, "npm"), { recursive: true });
fs.writeFileSync(path.join(appData, "npm", "node_modules"), "not a directory", "utf-8");
fs.mkdirSync(binDir, { recursive: true });
fs.writeFileSync(
path.join(binDir, "npm"),
`#!/bin/sh\ntouch "$PI_TEST_NPM_MARKER"\nprintf '%s\\n' "$PI_TEST_FALLBACK_ROOT"\n`,
{ encoding: "utf-8", mode: 0o755 },
);
fs.writeFileSync(
path.join(binDir, "npm.cmd"),
`@echo off\r\necho called > "%PI_TEST_NPM_MARKER%"\r\necho %PI_TEST_FALLBACK_ROOT%\r\n`,
"utf-8",
);
fs.writeFileSync(
path.join(binDir, "cmd.exe"),
`#!/bin/sh\nexec sh -c "npm root -g"\n`,
{ encoding: "utf-8", mode: 0o755 },
);
writeSkillFile(path.join(packageRoot, "skills", "fallback-global-skill"), "Use the fallback global skill.");
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({
name: "fallback-global-package",
pi: { skills: ["./skills"] },
"pi-subagents": { agents: ["./agents"] },
}, null, 2),
"utf-8",
);
fs.mkdirSync(path.join(packageRoot, "agents"), { recursive: true });
fs.writeFileSync(path.join(packageRoot, "agents", "fallback-global-agent.md"), `---
name: fallback-global-agent
description: Loaded from npm fallback global root.
---
Fallback global agent.
`, "utf-8");
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
const previousAppData = process.env.APPDATA;
const previousPath = process.env.PATH;
const previousComSpec = process.env.ComSpec;
const previousMarkerEnv = process.env.PI_TEST_NPM_MARKER;
const previousFallbackRootEnv = process.env.PI_TEST_FALLBACK_ROOT;
try {
Object.defineProperty(process, "platform", { value: "win32" });
process.env.APPDATA = appData;
process.env.PATH = `${binDir}${path.delimiter}${previousPath ?? ""}`;
if (os.platform() !== "win32") process.env.ComSpec = path.join(binDir, "cmd.exe");
process.env.PI_TEST_NPM_MARKER = marker;
process.env.PI_TEST_FALLBACK_ROOT = fallbackRoot;
const skills = await importSkillsFresh();
assert.ok(skills.discoverAvailableSkills(tempDir).some((skill) => skill.name === "fallback-global-skill"));
const agents = await importAgentsFresh();
assert.ok(agents.discoverAgents(tempDir, "both").agents.some((agent) => agent.name === "fallback-global-agent"));
assert.equal(fs.existsSync(marker), true, "npm root -g should run when APPDATA root is invalid");
} finally {
if (platformDescriptor) Object.defineProperty(process, "platform", platformDescriptor);
if (previousAppData === undefined) delete process.env.APPDATA;
else process.env.APPDATA = previousAppData;
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
if (previousComSpec === undefined) delete process.env.ComSpec;
else process.env.ComSpec = previousComSpec;
if (previousMarkerEnv === undefined) delete process.env.PI_TEST_NPM_MARKER;
else process.env.PI_TEST_NPM_MARKER = previousMarkerEnv;
if (previousFallbackRootEnv === undefined) delete process.env.PI_TEST_FALLBACK_ROOT;
else process.env.PI_TEST_FALLBACK_ROOT = previousFallbackRootEnv;
}
});
it("falls back to the runtime cwd when the execution cwd lacks the skill", () => {
const nestedDir = path.join(tempDir, "nested");
fs.mkdirSync(nestedDir, { recursive: true });
makePackageSkill(tempDir, "runtime-fallback-skill", "Runtime fallback skill.");
const { resolved, missing } = resolveSkillsWithFallback(["runtime-fallback-skill"], nestedDir, tempDir);
assert.deepEqual(missing, []);
assert.equal(resolved.length, 1);
assert.equal(resolved[0]?.source, "project-package");
});
it("discovers skills from user settings packages", async () => {
const fakeHome = path.join(tempDir, "fake-home");
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
const userPackageRoot = path.join(userAgentDir, "user-pkg");
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
try {
process.env.HOME = fakeHome;
process.env.USERPROFILE = fakeHome;
makePackageSkill(userPackageRoot, "user-settings-package-skill", "User settings package skill.");
fs.mkdirSync(userAgentDir, { recursive: true });
fs.writeFileSync(
path.join(userAgentDir, "settings.json"),
JSON.stringify({ packages: [{ source: "./user-pkg" }] }, null, 2),
"utf-8",
);
const fresh = await importSkillsFresh();
fresh.clearSkillCache();
const discovered = fresh.discoverAvailableSkills(tempDir);
const skill = discovered.find((entry) => entry.name === "user-settings-package-skill");
assert.ok(skill);
assert.equal(skill?.source, "user-package");
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
}
});
it("discovers skills from user settings git package sources", async () => {
const fakeHome = path.join(tempDir, "fake-home");
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
const packageRoot = path.join(userAgentDir, "git", "github.com", "user", "repo");
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
try {
process.env.HOME = fakeHome;
process.env.USERPROFILE = fakeHome;
makePackageSkill(packageRoot, "user-settings-git-package-skill", "User settings git package skill.");
fs.mkdirSync(userAgentDir, { recursive: true });
fs.writeFileSync(
path.join(userAgentDir, "settings.json"),
JSON.stringify({ packages: ["git:github.com/user/repo.git@main"] }, null, 2),
"utf-8",
);
const fresh = await importSkillsFresh();
fresh.clearSkillCache();
const discovered = fresh.discoverAvailableSkills(tempDir);
const skill = discovered.find((entry) => entry.name === "user-settings-git-package-skill");
assert.ok(skill);
assert.equal(skill?.source, "user-package");
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
}
});
it("discovers skills from user settings scoped npm package sources", async () => {
const fakeHome = path.join(tempDir, "fake-home");
const userAgentDir = path.join(fakeHome, ".selesai", "agent");
const packageRoot = path.join(userAgentDir, "npm", "node_modules", "@scope", "skill-package");
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
try {
process.env.HOME = fakeHome;
process.env.USERPROFILE = fakeHome;
makePackageSkill(
packageRoot,
"user-settings-scoped-npm-package-skill",
"User settings scoped npm package skill.",
"@scope/skill-package",
);
fs.mkdirSync(userAgentDir, { recursive: true });
fs.writeFileSync(
path.join(userAgentDir, "settings.json"),
JSON.stringify({ packages: [{ source: "npm:@scope/skill-package@latest" }] }, null, 2),
"utf-8",
);
const fresh = await importSkillsFresh();
fresh.clearSkillCache();
const discovered = fresh.discoverAvailableSkills(tempDir);
const skill = discovered.find((entry) => entry.name === "user-settings-scoped-npm-package-skill");
assert.ok(skill);
assert.equal(skill?.source, "user-package");
} finally {
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = previousUserProfile;
}
});
it("resolves agent-local files and directories before global skills without publishing them", () => {
makeProjectSkill(tempDir, "shared", "global body");
const agentDir = path.join(tempDir, "agents", "nested");
writeSkillFile(path.join(agentDir, "skills", "shared"), "local shared body");
writeSkillFile(path.join(agentDir, "direct"), "local direct body");
const local = resolveSkills(["shared", "direct", "missing"], tempDir, ["./skills", "./direct/SKILL.md"], agentDir);
assert.deepEqual(local.resolved.map((skill) => [skill.name, skill.content]), [
["shared", "local shared body"],
["direct", "local direct body"],
]);
assert.deepEqual(local.missing, ["missing"]);
assert.equal(resolveSkills(["shared"], tempDir).resolved[0]?.content, "global body");
assert.equal(discoverAvailableSkills(tempDir).some((skill) => skill.name === "direct"), false);
});
it("does not read malformed global settings when every selected local skill resolves", () => {
const agentDir = path.join(tempDir, "agents", "nested");
writeSkillFile(path.join(agentDir, "skills", "local"), "local body");
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(path.join(tempDir, ".selesai", "settings.json"), "{bad-json", "utf-8");
const result = resolveSkills(["local"], tempDir, ["./skills"], agentDir);
assert.deepEqual(result.missing, []);
assert.equal(result.resolved[0]?.content, "local body");
});
it("falls back globally when an agent-local skill candidate cannot be read", () => {
makeProjectSkill(tempDir, "shared", "global body");
const agentDir = path.join(tempDir, "agents", "nested");
const invalidLocalFile = path.join(agentDir, "skills", "shared", "SKILL.md");
fs.mkdirSync(invalidLocalFile, { recursive: true });
const result = resolveSkills(["shared"], tempDir, ["./skills"], agentDir);
assert.deepEqual(result.missing, []);
assert.equal(result.resolved[0]?.content, "global body");
});
it("keeps same-named agent-local skills isolated between invocations", () => {
makeProjectSkill(tempDir, "global-only", "global fallback");
const one = path.join(tempDir, "one");
const two = path.join(tempDir, "two");
writeSkillFile(path.join(one, "skills", "private"), "one private");
writeSkillFile(path.join(two, "skills", "private"), "two private");
assert.equal(resolveSkills(["private", "global-only"], tempDir, ["./skills"], one).resolved[0]?.content, "one private");
assert.equal(resolveSkills(["private", "global-only"], tempDir, ["./skills"], two).resolved[0]?.content, "two private");
assert.equal(resolveSkills(["global-only"], tempDir, ["./skills"], one).resolved[0]?.content, "global fallback");
});
it("surfaces malformed project settings files instead of silently ignoring them", () => {
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(path.join(tempDir, ".selesai", "settings.json"), "{bad-json", "utf-8");
assert.throws(
() => resolveSkills(["missing-skill"], tempDir),
/Failed to read skills settings file .+\.selesai[\\/]settings\.json/,
);
});
it("surfaces malformed explicit settings package manifests instead of silently ignoring them", () => {
const packageRoot = path.join(tempDir, ".selesai", "packages", "broken-package");
fs.mkdirSync(packageRoot, { recursive: true });
fs.writeFileSync(path.join(packageRoot, "package.json"), "{bad-json", "utf-8");
fs.mkdirSync(path.join(tempDir, ".selesai"), { recursive: true });
fs.writeFileSync(
path.join(tempDir, ".selesai", "settings.json"),
JSON.stringify({ packages: ["./packages/broken-package"] }, null, 2),
"utf-8",
);
assert.throws(
() => discoverAvailableSkills(tempDir),
/Failed to read package manifest .+broken-package[\\/]package\.json/,
);
});
});