forked from EduCraft/curriculum-project-hub
feat: make agent roles and skills dynamic
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { importSkillDirectory, prepareRunSkillPlugin } from "../../src/agent/skillStore.js";
|
||||
|
||||
describe("content-addressed Agent skill store", () => {
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
it("imports a skill into an immutable digest directory and materializes a selected run plugin", async () => {
|
||||
const root = await makeRoot();
|
||||
const source = await makeSkill(root, "typst", "Typst help");
|
||||
const storeRoot = join(root, "store");
|
||||
|
||||
const installed = await importSkillDirectory({ sourceDir: source, storeRoot });
|
||||
expect(installed).toMatchObject({ name: "typst", description: "Typst help" });
|
||||
expect(installed.contentDigest).toMatch(/^[a-f0-9]{64}$/);
|
||||
await expect(readFile(join(storeRoot, "versions", installed.contentDigest, "SKILL.md"), "utf8"))
|
||||
.resolves.toContain("name: typst");
|
||||
|
||||
const plugin = await prepareRunSkillPlugin({
|
||||
storeRoot,
|
||||
runId: "run-1",
|
||||
skills: [{ name: "typst", version: "0.15.0", contentDigest: installed.contentDigest }],
|
||||
});
|
||||
expect(plugin).not.toBeNull();
|
||||
expect(plugin?.skillIds).toEqual(["cph-runtime:typst"]);
|
||||
await expect(readFile(join(plugin!.root, "skills", "typst", "reference.md"), "utf8"))
|
||||
.resolves.toBe("reference\n");
|
||||
|
||||
await plugin?.cleanup();
|
||||
await expect(readFile(join(plugin!.root, ".claude-plugin", "plugin.json"), "utf8"))
|
||||
.rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("rejects symlinks and detects content tampering before a run", async () => {
|
||||
const root = await makeRoot();
|
||||
const source = await makeSkill(root, "outline", "Outline");
|
||||
await symlink(join(source, "reference.md"), join(source, "link.md"));
|
||||
await expect(importSkillDirectory({ sourceDir: source, storeRoot: join(root, "store") }))
|
||||
.rejects.toThrow(/symlink/);
|
||||
await rm(join(source, "link.md"));
|
||||
|
||||
const storeRoot = join(root, "store");
|
||||
const installed = await importSkillDirectory({ sourceDir: source, storeRoot });
|
||||
await writeFile(join(storeRoot, "versions", installed.contentDigest, "reference.md"), "tampered\n");
|
||||
await expect(prepareRunSkillPlugin({
|
||||
storeRoot,
|
||||
runId: "run-2",
|
||||
skills: [{ name: "outline", version: "1", contentDigest: installed.contentDigest }],
|
||||
})).rejects.toThrow(/content digest mismatch/);
|
||||
});
|
||||
|
||||
async function makeRoot(): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), "cph-skill-store-"));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
});
|
||||
|
||||
async function makeSkill(root: string, name: string, description: string): Promise<string> {
|
||||
const source = join(root, "source", name);
|
||||
await mkdir(source, { recursive: true });
|
||||
await writeFile(join(source, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n# ${name}\n`);
|
||||
await writeFile(join(source, "reference.md"), "reference\n");
|
||||
return source;
|
||||
}
|
||||
Reference in New Issue
Block a user