tests / toolsProvider.test.ts
tests / toolsProvider.test.ts
import { describe, expect, it } from "vitest";
import { createCatalog } from "../src/catalog";
import { listSkillFiles, listSkills, readSkillFile, resolveInside } from "../src/toolsProvider";
import { fakeFs } from "./fakeFs";
const ROOT = "/home/u/.agents/skills";
const fm = (name: string, desc: string) => `---\nname: ${name}\ndescription: ${desc}\n---\n# ${name}\n`;
function setup() {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: fm("pdf", "Work with PDF files."),
[`${ROOT}/pdf/references/forms.md`]: "# Forms\n",
[`${ROOT}/pdf/scripts/merge.py`]: "print('merge')\n",
[`${ROOT}/pdf/scripts/.hidden`]: "x",
[`${ROOT}/pdf/node_modules/dep/index.js`]: "x",
[`${ROOT}/pdf/a/b/c/d/e/deep.txt`]: "too deep",
[`${ROOT}/pdf/bin.dat`]: Buffer.from([0x41, 0x00, 0x42]),
[`${ROOT}/docx/SKILL.md`]: fm("docx", "Word documents."),
[`${ROOT}/docx/big.md`]: "x".repeat(50_000),
"/outside/secret.txt": "secret",
"/elsewhere/cua/SKILL.md": fm("cua", "Drive GUI apps."),
"/elsewhere/cua/scripts/run.sh": "echo hi\n",
},
symlinks: { [`${ROOT}/pdf/escape`]: "/outside", [`${ROOT}/cua-driver`]: "/elsewhere/cua" },
});
return { fs, index: () => createCatalog(fs).load(ROOT) };
}
describe("resolveInside", () => {
it("resolves relative paths inside the skill dir", async () => {
const { fs } = setup();
expect(await resolveInside(fs, `${ROOT}/pdf`, "references/forms.md")).toBe(`${ROOT}/pdf/references/forms.md`);
});
it("rejects .., absolute paths and symlinks that leave the dir", async () => {
const { fs } = setup();
await expect(resolveInside(fs, `${ROOT}/pdf`, "../docx/SKILL.md")).rejects.toThrow(/escapes/);
await expect(resolveInside(fs, `${ROOT}/pdf`, "/outside/secret.txt")).rejects.toThrow(/escapes/);
await expect(resolveInside(fs, `${ROOT}/pdf`, "escape/secret.txt")).rejects.toThrow(/escapes/);
});
it("resolves a missing file inside a symlinked skill dir to the real path instead of reporting an escape", async () => {
const { fs } = setup();
expect(await resolveInside(fs, `${ROOT}/cua-driver`, "missing.md")).toBe("/elsewhere/cua/missing.md");
});
it("still rejects escapes from a symlinked skill dir", async () => {
const { fs } = setup();
await expect(resolveInside(fs, `${ROOT}/cua-driver`, "../../home/u/.agents/skills/pdf/SKILL.md")).rejects.toThrow(/escapes/);
});
});
describe("listSkills", () => {
it("lists every skill with description and path", async () => {
const { index } = setup();
const out = listSkills(await index());
expect(out).toBe(
`cua — Drive GUI apps. — ${ROOT}/cua-driver\ndocx — Word documents. — ${ROOT}/docx\npdf — Work with PDF files. — ${ROOT}/pdf`,
);
});
it("filters by case-insensitive substring", async () => {
const { index } = setup();
expect(listSkills(await index(), "WORD")).toBe(`docx — Word documents. — ${ROOT}/docx`);
expect(listSkills(await index(), "zzz")).toBe("No skills match \"zzz\".");
});
it("appends notes", async () => {
const fs = fakeFs({ files: { [`${ROOT}/broken/SKILL.md`]: "---\nname: [x\n---\nbody" } });
const out = listSkills(await createCatalog(fs).load(ROOT));
expect(out).toBe(`broken — — ${ROOT}/broken [frontmatter unreadable]`);
});
it("reports an empty root", async () => {
const out = listSkills(await createCatalog(fakeFs({ files: {} })).load(ROOT));
expect(out).toBe(`No skills found under ${ROOT}.`);
});
});
describe("readSkillFile", () => {
it("defaults to SKILL.md", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf")).toBe(fm("pdf", "Work with PDF files."));
});
it("reads a nested file", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf", "references/forms.md")).toBe("# Forms\n");
});
it("returns Error strings for unknown skill, escape and missing file", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "nope")).toMatch(/^Error: unknown skill "nope"/);
expect(await readSkillFile(fs, await index(), "pdf", "../docx/SKILL.md")).toMatch(/^Error: .*escapes/);
expect(await readSkillFile(fs, await index(), "pdf", "missing.md")).toMatch(/^Error: /);
});
it("returns Error: ENOENT (not an escape) for a missing file in a symlinked skill", async () => {
const { fs, index } = setup();
const out = await readSkillFile(fs, await index(), "cua", "missing.md");
expect(out).toMatch(/^Error: ENOENT/);
expect(out).not.toMatch(/escapes/);
});
it("rejects binary files", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf", "bin.dat")).toBe("Error: bin.dat is a binary file.");
});
it("caps long files with a truncation note", async () => {
const { fs, index } = setup();
const out = await readSkillFile(fs, await index(), "docx", "big.md");
expect(out.length).toBeLessThan(50_000);
expect(out.endsWith("\n[truncated: showing the first 40000 of 50000 characters]")).toBe(true);
});
});
describe("listSkillFiles", () => {
it("lists files recursively with sizes, skipping dotfiles, node_modules and deep paths", async () => {
const { fs, index } = setup();
const out = await listSkillFiles(fs, await index(), "pdf");
expect(out.split("\n")).toEqual([
"SKILL.md (58)",
"a/b/c/d/ (dir)",
"bin.dat (3)",
"references/forms.md (8)",
"scripts/merge.py (15)",
]);
});
it("lists a subdirectory", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "pdf", "scripts")).toBe("scripts/merge.py (15)");
});
it("lists a symlinked skill with paths relative to the skill", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "cua")).toBe("SKILL.md (53)\nscripts/run.sh (8)");
});
it("returns Error strings for unknown skill and escapes", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "nope")).toMatch(/^Error: unknown skill/);
expect(await listSkillFiles(fs, await index(), "pdf", "../docx")).toMatch(/^Error: .*escapes/);
});
});
import { describe, expect, it } from "vitest";
import { createCatalog } from "../src/catalog";
import { listSkillFiles, listSkills, readSkillFile, resolveInside } from "../src/toolsProvider";
import { fakeFs } from "./fakeFs";
const ROOT = "/home/u/.agents/skills";
const fm = (name: string, desc: string) => `---\nname: ${name}\ndescription: ${desc}\n---\n# ${name}\n`;
function setup() {
const fs = fakeFs({
files: {
[`${ROOT}/pdf/SKILL.md`]: fm("pdf", "Work with PDF files."),
[`${ROOT}/pdf/references/forms.md`]: "# Forms\n",
[`${ROOT}/pdf/scripts/merge.py`]: "print('merge')\n",
[`${ROOT}/pdf/scripts/.hidden`]: "x",
[`${ROOT}/pdf/node_modules/dep/index.js`]: "x",
[`${ROOT}/pdf/a/b/c/d/e/deep.txt`]: "too deep",
[`${ROOT}/pdf/bin.dat`]: Buffer.from([0x41, 0x00, 0x42]),
[`${ROOT}/docx/SKILL.md`]: fm("docx", "Word documents."),
[`${ROOT}/docx/big.md`]: "x".repeat(50_000),
"/outside/secret.txt": "secret",
"/elsewhere/cua/SKILL.md": fm("cua", "Drive GUI apps."),
"/elsewhere/cua/scripts/run.sh": "echo hi\n",
},
symlinks: { [`${ROOT}/pdf/escape`]: "/outside", [`${ROOT}/cua-driver`]: "/elsewhere/cua" },
});
return { fs, index: () => createCatalog(fs).load(ROOT) };
}
describe("resolveInside", () => {
it("resolves relative paths inside the skill dir", async () => {
const { fs } = setup();
expect(await resolveInside(fs, `${ROOT}/pdf`, "references/forms.md")).toBe(`${ROOT}/pdf/references/forms.md`);
});
it("rejects .., absolute paths and symlinks that leave the dir", async () => {
const { fs } = setup();
await expect(resolveInside(fs, `${ROOT}/pdf`, "../docx/SKILL.md")).rejects.toThrow(/escapes/);
await expect(resolveInside(fs, `${ROOT}/pdf`, "/outside/secret.txt")).rejects.toThrow(/escapes/);
await expect(resolveInside(fs, `${ROOT}/pdf`, "escape/secret.txt")).rejects.toThrow(/escapes/);
});
it("resolves a missing file inside a symlinked skill dir to the real path instead of reporting an escape", async () => {
const { fs } = setup();
expect(await resolveInside(fs, `${ROOT}/cua-driver`, "missing.md")).toBe("/elsewhere/cua/missing.md");
});
it("still rejects escapes from a symlinked skill dir", async () => {
const { fs } = setup();
await expect(resolveInside(fs, `${ROOT}/cua-driver`, "../../home/u/.agents/skills/pdf/SKILL.md")).rejects.toThrow(/escapes/);
});
});
describe("listSkills", () => {
it("lists every skill with description and path", async () => {
const { index } = setup();
const out = listSkills(await index());
expect(out).toBe(
`cua — Drive GUI apps. — ${ROOT}/cua-driver\ndocx — Word documents. — ${ROOT}/docx\npdf — Work with PDF files. — ${ROOT}/pdf`,
);
});
it("filters by case-insensitive substring", async () => {
const { index } = setup();
expect(listSkills(await index(), "WORD")).toBe(`docx — Word documents. — ${ROOT}/docx`);
expect(listSkills(await index(), "zzz")).toBe("No skills match \"zzz\".");
});
it("appends notes", async () => {
const fs = fakeFs({ files: { [`${ROOT}/broken/SKILL.md`]: "---\nname: [x\n---\nbody" } });
const out = listSkills(await createCatalog(fs).load(ROOT));
expect(out).toBe(`broken — — ${ROOT}/broken [frontmatter unreadable]`);
});
it("reports an empty root", async () => {
const out = listSkills(await createCatalog(fakeFs({ files: {} })).load(ROOT));
expect(out).toBe(`No skills found under ${ROOT}.`);
});
});
describe("readSkillFile", () => {
it("defaults to SKILL.md", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf")).toBe(fm("pdf", "Work with PDF files."));
});
it("reads a nested file", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf", "references/forms.md")).toBe("# Forms\n");
});
it("returns Error strings for unknown skill, escape and missing file", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "nope")).toMatch(/^Error: unknown skill "nope"/);
expect(await readSkillFile(fs, await index(), "pdf", "../docx/SKILL.md")).toMatch(/^Error: .*escapes/);
expect(await readSkillFile(fs, await index(), "pdf", "missing.md")).toMatch(/^Error: /);
});
it("returns Error: ENOENT (not an escape) for a missing file in a symlinked skill", async () => {
const { fs, index } = setup();
const out = await readSkillFile(fs, await index(), "cua", "missing.md");
expect(out).toMatch(/^Error: ENOENT/);
expect(out).not.toMatch(/escapes/);
});
it("rejects binary files", async () => {
const { fs, index } = setup();
expect(await readSkillFile(fs, await index(), "pdf", "bin.dat")).toBe("Error: bin.dat is a binary file.");
});
it("caps long files with a truncation note", async () => {
const { fs, index } = setup();
const out = await readSkillFile(fs, await index(), "docx", "big.md");
expect(out.length).toBeLessThan(50_000);
expect(out.endsWith("\n[truncated: showing the first 40000 of 50000 characters]")).toBe(true);
});
});
describe("listSkillFiles", () => {
it("lists files recursively with sizes, skipping dotfiles, node_modules and deep paths", async () => {
const { fs, index } = setup();
const out = await listSkillFiles(fs, await index(), "pdf");
expect(out.split("\n")).toEqual([
"SKILL.md (58)",
"a/b/c/d/ (dir)",
"bin.dat (3)",
"references/forms.md (8)",
"scripts/merge.py (15)",
]);
});
it("lists a subdirectory", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "pdf", "scripts")).toBe("scripts/merge.py (15)");
});
it("lists a symlinked skill with paths relative to the skill", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "cua")).toBe("SKILL.md (53)\nscripts/run.sh (8)");
});
it("returns Error strings for unknown skill and escapes", async () => {
const { fs, index } = setup();
expect(await listSkillFiles(fs, await index(), "nope")).toMatch(/^Error: unknown skill/);
expect(await listSkillFiles(fs, await index(), "pdf", "../docx")).toMatch(/^Error: .*escapes/);
});
});