char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somadd skills extension for loading skillsa4e772a

main
5.6 KiB108 linesraw
1import { assert, assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context";
3import { getOrThrow } from "@earendil-works/pi-durable/env";
4import { rootedEnvironment } from "@berry/api/testing.ts";
5import { tempDir } from "@berry/util";
6import { discover, MAX_SKILL_BYTES, parseSkill, readSkill } from "./catalog.ts";
7import { hostEnvironment, writeSkill } from "./testing.ts";
8
9Deno.test("discovery combines target project skills, host XDG skills, and target user skills in precedence order", async () => {
10  await using root = await tempDir();
11  const env = rootedEnvironment(root.path, {
12    cwd: "/repo/packages/app",
13    variables: { XDG_CONFIG_HOME: "/settings" },
14  });
15  const host = hostEnvironment(root.path, { XDG_CONFIG_HOME: `${root.path}/host-settings` });
16  getOrThrow(await env.writeFile("/repo/.git", "gitdir: elsewhere", context));
17  await writeSkill(env, "/repo/packages/app/.agents/skills", "review", "nearest");
18  await writeSkill(env, "/repo/.agents/skills", "review", "repository");
19  await writeSkill(host, `${root.path}/host-settings/berry/skills`, "review", "berry host");
20  await writeSkill(host, `${root.path}/host-settings/berry/skills`, "release", "berry release");
21  await writeSkill(env, "/settings/berry/skills", "remote-only", "ignored target configuration");
22  await writeSkill(env, "/home/.agents/skills", "release", "shared release");
23  await writeSkill(env, "/home/.agents/skills", "pdf", "shared pdf");
24  await writeSkill(env, "/.agents/skills", "outside", "unrelated ancestor");
25
26  const catalog = await discover(env, host, context);
27  assertEquals(catalog.projectRoot, "/repo");
28  assertEquals(catalog.skills.map(({ name }) => name), ["review", "release", "pdf"]);
29  assertEquals(catalog.skills[0].directory, "/repo/packages/app/.agents/skills/review");
30  assertEquals(catalog.skills[1].directory, `${root.path}/host-settings/berry/skills/release`);
31  assertStringIncludes(catalog.diagnostics.join("\n"), "shadowed");
32
33  const untrusted = await discover(env, host, context, []);
34  assertEquals(untrusted.skills.find(({ name }) => name === "review")?.scope, "host");
35  assertEquals(
36    untrusted.skills.find(({ name }) => name === "review")?.directory,
37    `${root.path}/host-settings/berry/skills/review`,
38  );
39});
40
41Deno.test("outside a repository discovery stays at cwd and uses the host's default configuration", async () => {
42  await using root = await tempDir();
43  const env = rootedEnvironment(root.path, { cwd: "/work/app" });
44  await writeSkill(env, "/work/app/.agents/skills", "here", "local");
45  await writeSkill(env, "/work/.agents/skills", "ancestor", "unrelated");
46  const host = hostEnvironment(root.path);
47  await writeSkill(host, `${root.path}/.config/berry/skills`, "personal", "personal");
48  await writeSkill(
49    env,
50    "/home/.config/berry/skills",
51    "remote-only",
52    "ignored target configuration",
53  );
54  assertEquals((await discover(env, host, context)).skills.map(({ name }) => name), [
55    "here",
56    "personal",
57  ]);
58});
59
60Deno.test("discovery deduplicates symlinks and keeps resource paths relative to the skill directory", async () => {
61  await using root = await tempDir();
62  const env = rootedEnvironment(root.path, { cwd: "/repo" });
63  const location = await writeSkill(env, "/repo/.agents/skills", "review", "linked instructions");
64  await Deno.symlink(
65    `${root.path}/repo/.agents/skills/review`,
66    `${root.path}/repo/.agents/skills/review-alias`,
67  );
68  getOrThrow(await env.createDir("/home/.agents", { recursive: true }, context));
69  await Deno.symlink(`${root.path}/repo/.agents/skills`, `${root.path}/home/.agents/skills`);
70  const shared = "/repo/shared.md";
71  getOrThrow(await env.renameFile(location, shared, context));
72  await Deno.symlink(`${root.path}${shared}`, `${root.path}${location}`);
73
74  const catalog = await discover(env, hostEnvironment(root.path), context);
75  assertEquals(catalog.skills.length, 1);
76  assertEquals(catalog.skills[0].directory, "/repo/.agents/skills/review");
77  assert(!catalog.diagnostics.some((line) => line.includes("shadowed")));
78});
79
80Deno.test("malformed and oversized skills produce diagnostics without hiding valid skills", async () => {
81  await using root = await tempDir();
82  const env = rootedEnvironment(root.path, { cwd: "/repo" });
83  getOrThrow(
84    await env.writeFile("/repo/.agents/skills/broken/SKILL.md", "---\nname: [\n---\n", context),
85  );
86  const oversized = await writeSkill(
87    env,
88    "/repo/.agents/skills",
89    "huge",
90    "x".repeat(MAX_SKILL_BYTES),
91  );
92  await writeSkill(env, "/repo/.agents/skills", "n".repeat(65), "invalid name");
93  await writeSkill(env, "/repo/.agents/skills", "valid", "instructions");
94  const catalog = await discover(env, hostEnvironment(root.path), context);
95  assertEquals(catalog.skills.map(({ name }) => name), ["valid"]);
96  assertStringIncludes(catalog.diagnostics.join("\n"), "broken/SKILL.md");
97  assertStringIncludes(catalog.diagnostics.join("\n"), "exceeds");
98  assertStringIncludes(catalog.diagnostics.join("\n"), "at most 64");
99  await assertRejects(() => readSkill(env, oversized, context), Error, "exceeds");
100});
101
102Deno.test("multiline YAML and optional metadata remain compatible with standard skills", () => {
103  const content =
104    "---\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";
105  const parsed = parseSkill(content, "old-directory");
106  assertEquals(parsed.description, "Prepare a release: verify the changelog.");
107  assertStringIncludes(parsed.warnings.join("\n"), "old-directory");
108});