import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert"; import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; import { getOrThrow } from "@earendil-works/pi-durable/env"; import { rootedEnvironment } from "@berry/api/testing.ts"; import { tempDir } from "@berry/util"; import { discover, MAX_SKILL_BYTES, parseSkill, readSkill } from "./catalog.ts"; import { hostEnvironment, writeSkill } from "./testing.ts"; Deno.test("discovery combines target project skills, host XDG skills, and target user skills in precedence order", async () => { await using root = await tempDir(); const env = rootedEnvironment(root.path, { cwd: "/repo/packages/app", variables: { XDG_CONFIG_HOME: "/settings" }, }); const host = hostEnvironment(root.path, { XDG_CONFIG_HOME: `${root.path}/host-settings` }); getOrThrow(await env.writeFile("/repo/.git", "gitdir: elsewhere", context)); await writeSkill(env, "/repo/packages/app/.agents/skills", "review", "nearest"); await writeSkill(env, "/repo/.agents/skills", "review", "repository"); await writeSkill(host, `${root.path}/host-settings/berry/skills`, "review", "berry host"); await writeSkill(host, `${root.path}/host-settings/berry/skills`, "release", "berry release"); await writeSkill(env, "/settings/berry/skills", "remote-only", "ignored target configuration"); await writeSkill(env, "/home/.agents/skills", "release", "shared release"); await writeSkill(env, "/home/.agents/skills", "pdf", "shared pdf"); await writeSkill(env, "/.agents/skills", "outside", "unrelated ancestor"); const catalog = await discover(env, host, context); assertEquals(catalog.projectRoot, "/repo"); assertEquals(catalog.skills.map(({ name }) => name), ["review", "release", "pdf"]); assertEquals(catalog.skills[0].directory, "/repo/packages/app/.agents/skills/review"); assertEquals(catalog.skills[1].directory, `${root.path}/host-settings/berry/skills/release`); assertStringIncludes(catalog.diagnostics.join("\n"), "shadowed"); const untrusted = await discover(env, host, context, []); assertEquals(untrusted.skills.find(({ name }) => name === "review")?.scope, "host"); assertEquals( untrusted.skills.find(({ name }) => name === "review")?.directory, `${root.path}/host-settings/berry/skills/review`, ); }); Deno.test("outside a repository discovery stays at cwd and uses the host's default configuration", async () => { await using root = await tempDir(); const env = rootedEnvironment(root.path, { cwd: "/work/app" }); await writeSkill(env, "/work/app/.agents/skills", "here", "local"); await writeSkill(env, "/work/.agents/skills", "ancestor", "unrelated"); const host = hostEnvironment(root.path); await writeSkill(host, `${root.path}/.config/berry/skills`, "personal", "personal"); await writeSkill( env, "/home/.config/berry/skills", "remote-only", "ignored target configuration", ); assertEquals((await discover(env, host, context)).skills.map(({ name }) => name), [ "here", "personal", ]); }); Deno.test("discovery deduplicates symlinks and keeps resource paths relative to the skill directory", async () => { await using root = await tempDir(); const env = rootedEnvironment(root.path, { cwd: "/repo" }); const location = await writeSkill(env, "/repo/.agents/skills", "review", "linked instructions"); await Deno.symlink( `${root.path}/repo/.agents/skills/review`, `${root.path}/repo/.agents/skills/review-alias`, ); getOrThrow(await env.createDir("/home/.agents", { recursive: true }, context)); await Deno.symlink(`${root.path}/repo/.agents/skills`, `${root.path}/home/.agents/skills`); const shared = "/repo/shared.md"; getOrThrow(await env.renameFile(location, shared, context)); await Deno.symlink(`${root.path}${shared}`, `${root.path}${location}`); const catalog = await discover(env, hostEnvironment(root.path), context); assertEquals(catalog.skills.length, 1); assertEquals(catalog.skills[0].directory, "/repo/.agents/skills/review"); assert(!catalog.diagnostics.some((line) => line.includes("shadowed"))); }); Deno.test("malformed and oversized skills produce diagnostics without hiding valid skills", async () => { await using root = await tempDir(); const env = rootedEnvironment(root.path, { cwd: "/repo" }); getOrThrow( await env.writeFile("/repo/.agents/skills/broken/SKILL.md", "---\nname: [\n---\n", context), ); const oversized = await writeSkill( env, "/repo/.agents/skills", "huge", "x".repeat(MAX_SKILL_BYTES), ); await writeSkill(env, "/repo/.agents/skills", "n".repeat(65), "invalid name"); await writeSkill(env, "/repo/.agents/skills", "valid", "instructions"); const catalog = await discover(env, hostEnvironment(root.path), context); assertEquals(catalog.skills.map(({ name }) => name), ["valid"]); assertStringIncludes(catalog.diagnostics.join("\n"), "broken/SKILL.md"); assertStringIncludes(catalog.diagnostics.join("\n"), "exceeds"); assertStringIncludes(catalog.diagnostics.join("\n"), "at most 64"); await assertRejects(() => readSkill(env, oversized, context), Error, "exceeds"); }); Deno.test("multiline YAML and optional metadata remain compatible with standard skills", () => { const content = "---\r\nname: release\r\ndescription: >-\r\n Prepare a release:\r\n verify the changelog.\r\nmetadata:\r\n author: charlotte\r\nallowed-tools: Bash Read\r\n---\r\nHuman prose: keep this spacing.\r\n"; const parsed = parseSkill(content, "old-directory"); assertEquals(parsed.description, "Prepare a release: verify the changelog."); assertStringIncludes(parsed.warnings.join("\n"), "old-directory"); });