tests / catalog.test.ts
tests / catalog.test.ts
import { describe, expect, it } from "vitest";
import { createCatalog, splitFrontmatter } from "../src/catalog";
import { fakeFs } from "./fakeFs";
const ROOT = "/home/u/.agents/skills";
function skillMd(name: string, description: string, extra = "") {
return `---\nname: ${name}\ndescription: >\n ${description}\n${extra}---\n\n# ${name}\n\nBody of ${name}.\n`;
}
describe("splitFrontmatter", () => {
it("separates YAML frontmatter from the body", () => {
const r = splitFrontmatter("---\nname: x\ndescription: hi\n---\n\n# X\n");
expect(r.data).toEqual({ name: "x", description: "hi" });
expect(r.body).toBe("# X\n");
expect(r.error).toBeUndefined();
});
it("returns the whole text as body when there is no frontmatter", () => {
const r = splitFrontmatter("# no fm\n");
expect(r.data).toBeNull();
expect(r.body).toBe("# no fm\n");
});
it("ignores a leading UTF-8 BOM", () => {
const r = splitFrontmatter("\uFEFF---\nname: x\n---\nbody");
expect(r.data?.name).toBe("x");
expect(r.body).toBe("body");
});
it("reports a parse error and keeps the body", () => {
const r = splitFrontmatter("---\nname: [unclosed\n---\nbody\n");
expect(r.data).toBeNull();
expect(r.error).toMatch(/./);
expect(r.body).toBe("body\n");
});
});
describe("createCatalog", () => {
it("indexes folders with SKILL.md and skips others", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "Work with PDF files."),
[`${ROOT}/docx/SKILL.md`]: skillMd("docx", "Word documents."),
[`${ROOT}/notes.txt`]: "not a skill",
[`${ROOT}/empty/readme.md`]: "no SKILL.md here",
},
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills.map(s => s.name)).toEqual(["docx", "pdf"]);
expect(index.byName.get("pdf")).toMatchObject({
description: "Work with PDF files.",
dir: `${ROOT}/pdf`,
skillMdPath: `${ROOT}/pdf/SKILL.md`,
routable: true,
disableModelInvocation: false,
});
expect(index.routable.map(s => s.name).sort()).toEqual(["docx", "pdf"]);
});
it("falls back to the folder name and marks unroutable when frontmatter is unreadable", async () => {
const log: string[] = [];
const fs = fakeFs({ files: { [`${ROOT}/broken/SKILL.md`]: "---\nname: [oops\n---\nbody" } });
const index = await createCatalog(fs, m => log.push(m)).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "broken", description: "", routable: false, note: "frontmatter unreadable" });
expect(log.join("\n")).toMatch(/broken/);
});
it("marks skills without a description unroutable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/nodesc/SKILL.md`]: "---\nname: nodesc\n---\nbody" } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0].routable).toBe(false);
expect(index.routable).toEqual([]);
});
it("falls back to the folder name when the frontmatter name is not activatable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/pdf/SKILL.md`]: skillMd('"PDF Tools"', "Work with PDF files.") } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "pdf", note: "invalid name in frontmatter", routable: true });
expect(index.byName.has("pdf")).toBe(true);
expect(index.routable.map(s => s.name)).toEqual(["pdf"]);
});
it("marks a skill whose folder name is not activatable as unroutable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/my.skill/SKILL.md`]: "---\ndescription: Dotted folder.\n---\nbody" } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "my.skill", routable: false, note: "name not activatable" });
expect(index.routable).toEqual([]);
});
it("honours disable-model-invocation", async () => {
const fs = fakeFs({
files: { [`${ROOT}/manual/SKILL.md`]: skillMd("manual", "Only on request.", "disable-model-invocation: true\n") },
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ disableModelInvocation: true, routable: false });
expect(index.byName.has("manual")).toBe(true);
});
it("keeps the first of two skills with the same name and notes the duplicate", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/a-folder/SKILL.md`]: skillMd("same", "First."),
[`${ROOT}/b-folder/SKILL.md`]: skillMd("same", "Second."),
},
});
const index = await createCatalog(fs).load(ROOT);
expect(index.byName.get("same")?.dir).toBe(`${ROOT}/a-folder`);
const dup = index.skills.find(s => s.dir === `${ROOT}/b-folder`)!;
expect(dup.routable).toBe(false);
expect(dup.note).toBe(`duplicate name, folder: ${ROOT}/b-folder`);
expect(index.routable).toHaveLength(1);
});
it("returns an empty index when the root is missing", async () => {
const fs = fakeFs({ files: {} });
const index = await createCatalog(fs).load(ROOT);
expect(index).toMatchObject({ root: ROOT, skills: [], routable: [] });
});
it("caches until a SKILL.md or the root changes", async () => {
const fs = fakeFs({ files: { [`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "PDFs.") } });
const catalog = createCatalog(fs);
await catalog.load(ROOT);
const readsAfterFirst = fs.calls.readFile;
await catalog.load(ROOT);
expect(fs.calls.readFile).toBe(readsAfterFirst);
fs.write(`${ROOT}/pdf/SKILL.md`, skillMd("pdf", "PDF files, updated."));
const index = await catalog.load(ROOT);
expect(fs.calls.readFile).toBeGreaterThan(readsAfterFirst);
expect(index.byName.get("pdf")?.description).toBe("PDF files, updated.");
});
it("indexes a skill whose folder is a symlink", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "Work with PDF files."),
"/elsewhere/cua/SKILL.md": skillMd("cua", "Drive GUI apps."),
},
symlinks: { [`${ROOT}/cua-driver`]: "/elsewhere/cua" },
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills.map(s => s.name).sort()).toEqual(["cua", "pdf"]);
expect(index.byName.get("cua")).toMatchObject({
dir: `${ROOT}/cua-driver`,
skillMdPath: `${ROOT}/cua-driver/SKILL.md`,
routable: true,
});
});
it("keeps a skill whose SKILL.md cannot be read, unroutable with a note", async () => {
const base = fakeFs({
files: {
[`${ROOT}/bad/SKILL.md`]: skillMd("bad", "Unreadable on disk."),
[`${ROOT}/good/SKILL.md`]: skillMd("good", "Readable."),
},
});
const failing = {
...base,
readFile: async (p: string) => {
if (p.endsWith("/bad/SKILL.md")) throw Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
return base.readFile(p);
},
};
const log: string[] = [];
const index = await createCatalog(failing, m => log.push(m)).load(ROOT);
expect(index.skills.map(s => s.name)).toEqual(["bad", "good"]);
expect(index.byName.get("bad")).toMatchObject({
name: "bad",
description: "",
routable: false,
disableModelInvocation: false,
note: "SKILL.md unreadable",
});
expect(index.byName.get("good")?.routable).toBe(true);
expect(index.routable.map(s => s.name)).toEqual(["good"]);
expect(log.join("\n")).toMatch(/bad\/SKILL\.md/);
expect(log.join("\n")).toMatch(/EACCES/);
});
it("routes at most 40 skills, preferring the most recently modified", async () => {
const files: Record<string, string> = {};
const mtimes: Record<string, number> = {};
for (let i = 0; i < 45; i++) {
const n = `s${String(i).padStart(2, "0")}`;
files[`${ROOT}/${n}/SKILL.md`] = skillMd(n, `Skill ${n}.`);
mtimes[`${ROOT}/${n}/SKILL.md`] = 1000 + i;
}
const log: string[] = [];
const index = await createCatalog(fakeFs({ files, mtimes }), m => log.push(m)).load(ROOT);
expect(index.skills).toHaveLength(45);
expect(index.routable).toHaveLength(40);
expect(index.routable.map(s => s.name)).not.toContain("s00");
expect(index.routable[0].name).toBe("s44");
expect(log.some(m => /40/.test(m))).toBe(true);
});
});
import { describe, expect, it } from "vitest";
import { createCatalog, splitFrontmatter } from "../src/catalog";
import { fakeFs } from "./fakeFs";
const ROOT = "/home/u/.agents/skills";
function skillMd(name: string, description: string, extra = "") {
return `---\nname: ${name}\ndescription: >\n ${description}\n${extra}---\n\n# ${name}\n\nBody of ${name}.\n`;
}
describe("splitFrontmatter", () => {
it("separates YAML frontmatter from the body", () => {
const r = splitFrontmatter("---\nname: x\ndescription: hi\n---\n\n# X\n");
expect(r.data).toEqual({ name: "x", description: "hi" });
expect(r.body).toBe("# X\n");
expect(r.error).toBeUndefined();
});
it("returns the whole text as body when there is no frontmatter", () => {
const r = splitFrontmatter("# no fm\n");
expect(r.data).toBeNull();
expect(r.body).toBe("# no fm\n");
});
it("ignores a leading UTF-8 BOM", () => {
const r = splitFrontmatter("\uFEFF---\nname: x\n---\nbody");
expect(r.data?.name).toBe("x");
expect(r.body).toBe("body");
});
it("reports a parse error and keeps the body", () => {
const r = splitFrontmatter("---\nname: [unclosed\n---\nbody\n");
expect(r.data).toBeNull();
expect(r.error).toMatch(/./);
expect(r.body).toBe("body\n");
});
});
describe("createCatalog", () => {
it("indexes folders with SKILL.md and skips others", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "Work with PDF files."),
[`${ROOT}/docx/SKILL.md`]: skillMd("docx", "Word documents."),
[`${ROOT}/notes.txt`]: "not a skill",
[`${ROOT}/empty/readme.md`]: "no SKILL.md here",
},
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills.map(s => s.name)).toEqual(["docx", "pdf"]);
expect(index.byName.get("pdf")).toMatchObject({
description: "Work with PDF files.",
dir: `${ROOT}/pdf`,
skillMdPath: `${ROOT}/pdf/SKILL.md`,
routable: true,
disableModelInvocation: false,
});
expect(index.routable.map(s => s.name).sort()).toEqual(["docx", "pdf"]);
});
it("falls back to the folder name and marks unroutable when frontmatter is unreadable", async () => {
const log: string[] = [];
const fs = fakeFs({ files: { [`${ROOT}/broken/SKILL.md`]: "---\nname: [oops\n---\nbody" } });
const index = await createCatalog(fs, m => log.push(m)).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "broken", description: "", routable: false, note: "frontmatter unreadable" });
expect(log.join("\n")).toMatch(/broken/);
});
it("marks skills without a description unroutable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/nodesc/SKILL.md`]: "---\nname: nodesc\n---\nbody" } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0].routable).toBe(false);
expect(index.routable).toEqual([]);
});
it("falls back to the folder name when the frontmatter name is not activatable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/pdf/SKILL.md`]: skillMd('"PDF Tools"', "Work with PDF files.") } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "pdf", note: "invalid name in frontmatter", routable: true });
expect(index.byName.has("pdf")).toBe(true);
expect(index.routable.map(s => s.name)).toEqual(["pdf"]);
});
it("marks a skill whose folder name is not activatable as unroutable", async () => {
const fs = fakeFs({ files: { [`${ROOT}/my.skill/SKILL.md`]: "---\ndescription: Dotted folder.\n---\nbody" } });
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ name: "my.skill", routable: false, note: "name not activatable" });
expect(index.routable).toEqual([]);
});
it("honours disable-model-invocation", async () => {
const fs = fakeFs({
files: { [`${ROOT}/manual/SKILL.md`]: skillMd("manual", "Only on request.", "disable-model-invocation: true\n") },
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills[0]).toMatchObject({ disableModelInvocation: true, routable: false });
expect(index.byName.has("manual")).toBe(true);
});
it("keeps the first of two skills with the same name and notes the duplicate", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/a-folder/SKILL.md`]: skillMd("same", "First."),
[`${ROOT}/b-folder/SKILL.md`]: skillMd("same", "Second."),
},
});
const index = await createCatalog(fs).load(ROOT);
expect(index.byName.get("same")?.dir).toBe(`${ROOT}/a-folder`);
const dup = index.skills.find(s => s.dir === `${ROOT}/b-folder`)!;
expect(dup.routable).toBe(false);
expect(dup.note).toBe(`duplicate name, folder: ${ROOT}/b-folder`);
expect(index.routable).toHaveLength(1);
});
it("returns an empty index when the root is missing", async () => {
const fs = fakeFs({ files: {} });
const index = await createCatalog(fs).load(ROOT);
expect(index).toMatchObject({ root: ROOT, skills: [], routable: [] });
});
it("caches until a SKILL.md or the root changes", async () => {
const fs = fakeFs({ files: { [`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "PDFs.") } });
const catalog = createCatalog(fs);
await catalog.load(ROOT);
const readsAfterFirst = fs.calls.readFile;
await catalog.load(ROOT);
expect(fs.calls.readFile).toBe(readsAfterFirst);
fs.write(`${ROOT}/pdf/SKILL.md`, skillMd("pdf", "PDF files, updated."));
const index = await catalog.load(ROOT);
expect(fs.calls.readFile).toBeGreaterThan(readsAfterFirst);
expect(index.byName.get("pdf")?.description).toBe("PDF files, updated.");
});
it("indexes a skill whose folder is a symlink", async () => {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: skillMd("pdf", "Work with PDF files."),
"/elsewhere/cua/SKILL.md": skillMd("cua", "Drive GUI apps."),
},
symlinks: { [`${ROOT}/cua-driver`]: "/elsewhere/cua" },
});
const index = await createCatalog(fs).load(ROOT);
expect(index.skills.map(s => s.name).sort()).toEqual(["cua", "pdf"]);
expect(index.byName.get("cua")).toMatchObject({
dir: `${ROOT}/cua-driver`,
skillMdPath: `${ROOT}/cua-driver/SKILL.md`,
routable: true,
});
});
it("keeps a skill whose SKILL.md cannot be read, unroutable with a note", async () => {
const base = fakeFs({
files: {
[`${ROOT}/bad/SKILL.md`]: skillMd("bad", "Unreadable on disk."),
[`${ROOT}/good/SKILL.md`]: skillMd("good", "Readable."),
},
});
const failing = {
...base,
readFile: async (p: string) => {
if (p.endsWith("/bad/SKILL.md")) throw Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
return base.readFile(p);
},
};
const log: string[] = [];
const index = await createCatalog(failing, m => log.push(m)).load(ROOT);
expect(index.skills.map(s => s.name)).toEqual(["bad", "good"]);
expect(index.byName.get("bad")).toMatchObject({
name: "bad",
description: "",
routable: false,
disableModelInvocation: false,
note: "SKILL.md unreadable",
});
expect(index.byName.get("good")?.routable).toBe(true);
expect(index.routable.map(s => s.name)).toEqual(["good"]);
expect(log.join("\n")).toMatch(/bad\/SKILL\.md/);
expect(log.join("\n")).toMatch(/EACCES/);
});
it("routes at most 40 skills, preferring the most recently modified", async () => {
const files: Record<string, string> = {};
const mtimes: Record<string, number> = {};
for (let i = 0; i < 45; i++) {
const n = `s${String(i).padStart(2, "0")}`;
files[`${ROOT}/${n}/SKILL.md`] = skillMd(n, `Skill ${n}.`);
mtimes[`${ROOT}/${n}/SKILL.md`] = 1000 + i;
}
const log: string[] = [];
const index = await createCatalog(fakeFs({ files, mtimes }), m => log.push(m)).load(ROOT);
expect(index.skills).toHaveLength(45);
expect(index.routable).toHaveLength(40);
expect(index.routable.map(s => s.name)).not.toContain("s00");
expect(index.routable[0].name).toBe("s44");
expect(log.some(m => /40/.test(m))).toBe(true);
});
});