Skip to content
449 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync, readdirSync } from "node:fs";
3import { DatabaseSync } from "node:sqlite";
4import { test } from "node:test";
5
6import { zip as deflateZip } from "../../../apps/web/app/lib/zip.ts";
7import { type CheckedSkill, checkSkillFolder } from "../../../packages/contracts/src/skill-format.ts";
8import { draftedSkill, transcriptText } from "./skill-draft.ts";
9import { Library, type LibraryPorts, onPush } from "./skill-library.ts";
10import { bytesBase64, readUpload, unzip } from "./skill-zip.ts";
11import { zip as storedZip } from "./ooxml.ts";
12import { attachedRows, shelfFrom } from "./skills.ts";
13
14/** D1 over node's SQLite with the service's migrations: prepare, bind, first, run, all and batch. */
15function fakeD1(): D1Database {
16 const db = new DatabaseSync(":memory:");
17 const dir = new URL("../migrations/", import.meta.url);
18 for (const file of readdirSync(dir).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
19 const statement = (sql: string, params: unknown[] = []): any => ({
20 sql,
21 params,
22 bind: (...values: unknown[]) => statement(sql, values),
23 first: async () => (db.prepare(sql).get(...(params as never[])) as unknown) ?? null,
24 run: async () => ({ meta: { changes: Number(db.prepare(sql).run(...(params as never[])).changes) } }),
25 all: async () => ({ results: db.prepare(sql).all(...(params as never[])) }),
26 });
27 return {
28 prepare: (sql: string) => statement(sql),
29 batch: async (statements: any[]) => {
30 db.exec("BEGIN");
31 try {
32 const out = statements.map((s) => ({ meta: { changes: Number(db.prepare(s.sql).run(...(s.params as never[])).changes) } }));
33 db.exec("COMMIT");
34 return out;
35 } catch (error) {
36 db.exec("ROLLBACK");
37 throw error;
38 }
39 },
40 } as unknown as D1Database;
41}
42
43const WS = "wsp_acme";
44const at = new Date("2026-10-10T12:00:00Z");
45
46async function addAgent(db: D1Database, id: string, handle: string, team: string | null = null): Promise<void> {
47 await db
48 .prepare(
49 `INSERT INTO agents (id, workspace_id, handle, display_name, role, instructions, routing, budget, autonomy, created_by, created_at, updated_at, team)
50 VALUES (?, ?, ?, ?, 'r', 'i', '{}', '{}', '{}', 'ana', ?, ?, ?)`,
51 )
52 .bind(id, WS, handle, handle[0]!.toUpperCase() + handle.slice(1), at.toISOString(), at.toISOString(), team)
53 .run();
54}
55
56type Repo = { id: string; full: string; default_branch: string; commits: Record<string, Record<string, string>> };
57
58function ports(over: Partial<LibraryPorts> & { repos?: Repo[]; teamList?: { slug: string; name: string; can_manage: boolean }[]; log?: string[] } = {}): LibraryPorts {
59 const repos = over.repos ?? [];
60 const blobs = new Map<string, string>();
61 return {
62 teams: async () => over.teamList ?? [{ slug: "qa", name: "QA", can_manage: false }],
63 repo: async (full) => repos.find((r) => r.full === full) ?? null,
64 listFiles: async (repoId, ref) => {
65 const repo = repos.find((r) => r.id === repoId)!;
66 const commit = ref ?? repo.default_branch;
67 const files = repo.commits[commit];
68 if (!files) return { commit: null, files: [], truncated: false };
69 return {
70 commit: `${commit}-sha`,
71 files: Object.entries(files).map(([path, content]) => {
72 const hash = `h:${content.length}:${path}:${content}`;
73 blobs.set(hash, Buffer.from(content).toString("base64"));
74 return { path, hash };
75 }),
76 truncated: false,
77 };
78 },
79 blobs: async (_repoId, hashes) => hashes.map((hash) => ({ hash, data: blobs.get(hash) ?? null })),
80 agentTeams: async (agent) => (agent.team ? [{ slug: agent.team, name: agent.team.toUpperCase() }] : []),
81 audit: (action, name) => over.log?.push(`${action} ${name}`),
82 ...over,
83 };
84}
85
86function library(db: D1Database, who: { username: string; owner: boolean }, p: LibraryPorts = ports()): Library {
87 return new Library({ db, workspaceId: WS, slug: "acme", viewer: { id: `usr_${who.username}`, username: who.username }, owner: who.owner, ports: p, now: at });
88}
89
90const owner = { username: "chase", owner: true };
91const maintainer = { username: "mia", owner: false };
92const member = { username: "bo", owner: false };
93const maintainerPorts = () => ports({ teamList: [{ slug: "qa", name: "QA", can_manage: true }, { slug: "web", name: "Web", can_manage: false }] });
94
95const notes = {
96 name: "release-notes",
97 description: "Use when someone asks for release notes.",
98 instructions: "# Release notes\n\nGroup changes by area.",
99 tools: ["recent_activity", "create_artifact"],
100};
101
102test("an owner writes a skill; every save is a version, and attachments it may change follow", async () => {
103 const db = fakeD1();
104 await addAgent(db, "agt_margo", "margo", "qa");
105 const log: string[] = [];
106 const lib = library(db, owner, ports({ log }));
107 const made = await lib.save(null, notes);
108 assert.ok(made.ok, !made.ok ? made.error.message : "");
109 assert.equal(made.value.skill.version, 1);
110 assert.equal(made.value.skill.status, "published");
111 assert.deepEqual(made.value.tools, ["recent_activity", "create_artifact"]);
112 assert.match(made.value.skill_md, /^---\nname: release-notes\ndescription: Use when someone asks for release notes\.\ntools: \[recent_activity, create_artifact\]\n---\n\n# Release notes/);
113 assert.equal(made.value.instructions, "# Release notes\n\nGroup changes by area.");
114 assert.deepEqual(made.value.skill.origin, { kind: "written" });
115
116 const attached = await lib.attach("release-notes", "agent", "@margo");
117 assert.ok(attached.ok);
118 assert.deepEqual(
119 attached.value.skill.attachments.map((a) => [a.scope, a.target, a.label, a.version]),
120 [["agent", "margo", "@margo", 1]],
121 );
122 assert.equal((await lib.attach("release-notes", "agent", "margo")).ok, false, "attached there already");
123 assert.ok((await lib.attach("release-notes", "workspace", null)).ok);
124
125 // Saving the same thing again writes nothing.
126 const same = await lib.save("release-notes", notes);
127 assert.ok(same.ok && same.value.skill.version === 1);
128
129 // A change: version 2, and both attachments move with it.
130 const changed = await lib.save("release-notes", { ...notes, instructions: `${notes.instructions}\n\nLink every pull request.`, note: " Links " });
131 assert.ok(changed.ok);
132 assert.equal(changed.value.skill.version, 2);
133 assert.deepEqual(changed.value.skill.attachments.map((a) => a.version), [2, 2]);
134 assert.deepEqual(changed.value.versions.map((v) => [v.version, v.note]), [[2, "Links"], [1, null]]);
135 // Without moving them: version 3, attachments stay on 2, so an update is available.
136 const kept = await lib.save("release-notes", { ...notes, instructions: "# Release notes\n\nShort.", update_attachments: false });
137 assert.ok(kept.ok);
138 assert.deepEqual(kept.value.skill.attachments.map((a) => a.version), [2, 2]);
139 const tab = await lib.agentSkills("margo");
140 assert.ok(tab.ok);
141 const line = tab.value.skills.find((s) => s.name === "release-notes")!;
142 assert.equal(line.via, "agent", "attached to the agent itself, which wins over the workspace");
143 assert.equal(line.version, "2");
144 assert.equal(line.update, 3);
145 assert.equal(tab.value.skills.filter((s) => s.foundational).length, 6);
146 // The pin moves when someone asks; an old version can be pinned too.
147 const pinned = await lib.pin("release-notes", line.attachment_id, null);
148 assert.ok(pinned.ok);
149 assert.equal(pinned.value.skill.attachments.find((a) => a.scope === "agent")!.version, 3);
150 assert.ok((await lib.pin("release-notes", line.attachment_id, 1)).ok);
151 assert.equal((await lib.pin("release-notes", line.attachment_id, 9)).ok, false);
152 // An older version reads as it was.
153 const v1 = await lib.detail("release-notes", 1);
154 assert.ok(v1.ok && v1.value.shown === 1 && v1.value.instructions === notes.instructions);
155 assert.deepEqual(log, ["create_skill release-notes", "attach_skill release-notes", "attach_skill release-notes", "update_skill release-notes", "update_skill release-notes", "pin_skill release-notes", "pin_skill release-notes"]);
156});
157
158test("the editor's checks: names, the foundational names, tools agents have, and renames", async () => {
159 const db = fakeD1();
160 const lib = library(db, owner);
161 const bad = async (input: Partial<typeof notes>, pattern: RegExp) => {
162 const result = await lib.save(null, { ...notes, ...input });
163 assert.equal(result.ok, false, JSON.stringify(input));
164 assert.match(!result.ok ? result.error.message : "", pattern);
165 };
166 await bad({ name: "Release Notes" }, /lowercase letters, digits and single hyphens/);
167 await bad({ name: "documents" }, /one of g1t's foundational skills/);
168 await bad({ description: "" }, /needs a description/);
169 await bad({ instructions: "" }, /needs instructions/);
170 await bad({ tools: ["teleport"] }, /teleport, which isn't a tool agents have/);
171 assert.ok((await lib.save(null, notes)).ok);
172 const twice = await lib.save(null, notes);
173 assert.equal(!twice.ok && twice.error.code, "conflict");
174 // Files: added by path, removed by path, the rest kept.
175 const withFiles = await lib.save("release-notes", { ...notes, add_files: [{ path: "resources/a.md", content: "A" }, { path: "scripts/run.sh", content: "echo" }] });
176 assert.ok(withFiles.ok);
177 assert.deepEqual(withFiles.value.files.map((f) => f.path), ["resources/a.md", "scripts/run.sh"]);
178 assert.equal(withFiles.value.requires_computer, true, "a script needs a computer");
179 const fewer = await lib.save("release-notes", { ...notes, remove_files: ["scripts/run.sh"], add_files: [{ path: "resources/a.md", content: "A2" }] });
180 assert.ok(fewer.ok);
181 assert.deepEqual(fewer.value.files.map((f) => [f.path, f.content]), [["resources/a.md", "A2"]]);
182 assert.equal(fewer.value.requires_computer, false);
183 // Renaming keeps the history.
184 const renamed = await lib.save("release-notes", { ...notes, name: "changelog" });
185 assert.ok(renamed.ok);
186 assert.equal(renamed.value.skill.name, "changelog");
187 assert.equal(renamed.value.skill.version, 4);
188});
189
190test("who may do what: owners anything, maintainers their own skills and teams, members only look", async () => {
191 const db = fakeD1();
192 await addAgent(db, "agt_margo", "margo", "qa");
193 const asOwner = library(db, owner);
194 const asMaintainer = library(db, maintainer, maintainerPorts());
195 const asMember = library(db, member);
196
197 const view = await asMember.library();
198 assert.ok(view.ok);
199 assert.equal(view.value.can_write, false);
200 assert.deepEqual(view.value.teams, []);
201 assert.equal((await asMember.save(null, notes)).ok, false);
202
203 const mine = await asMaintainer.save(null, { ...notes, name: "qa-triage" });
204 assert.ok(mine.ok);
205 assert.equal(mine.value.skill.can_edit, true);
206 const lib = await asMaintainer.library();
207 assert.ok(lib.ok);
208 assert.equal(lib.value.can_write, true);
209 assert.equal(lib.value.can_manage, false);
210 assert.deepEqual(lib.value.teams, [{ slug: "qa", name: "QA" }], "only the teams they maintain");
211 assert.deepEqual(lib.value.agents, [], "only owners attach to agents");
212
213 // Their team: yes. Another team, an agent, the workspace: no.
214 assert.ok((await asMaintainer.attach("qa-triage", "team", "qa")).ok);
215 assert.equal((await asMaintainer.attach("qa-triage", "team", "web")).ok, false);
216 assert.equal((await asMaintainer.attach("qa-triage", "agent", "margo")).ok, false);
217 assert.equal((await asMaintainer.attach("qa-triage", "workspace", null)).ok, false);
218
219 // An owner's skill: a maintainer can't edit it, but can attach it to their team.
220 assert.ok((await asOwner.save(null, notes)).ok);
221 assert.equal((await asMaintainer.save("release-notes", { ...notes, description: "Use when x." })).ok, false);
222 const theirs = await asMaintainer.attach("release-notes", "team", "qa");
223 assert.ok(theirs.ok);
224 assert.equal(theirs.value.skill.can_edit, false);
225 // Owners attach anywhere; the maintainer can't detach the workspace-wide one.
226 const wide = await asOwner.attach("release-notes", "workspace", null);
227 assert.ok(wide.ok);
228 const wideId = wide.value.skill.attachments.find((a) => a.scope === "workspace")!.id;
229 assert.equal((await asMaintainer.detach("release-notes", wideId)).ok, false);
230 // Deleting: a maintainer's own skill used only by their team, yes; an owner's, no.
231 assert.equal((await asMaintainer.remove("release-notes")).ok, false);
232 assert.ok((await asMaintainer.remove("qa-triage")).ok);
233 assert.ok((await asOwner.remove("release-notes")).ok);
234 const after = await asOwner.library();
235 assert.deepEqual(after.ok && after.value.skills, []);
236 // A deleted skill's name is free again.
237 assert.ok((await asOwner.save(null, notes)).ok);
238});
239
240test("an agent's skills: once each, through its teams and the workspace, never past 100, published only", async () => {
241 const db = fakeD1();
242 await addAgent(db, "agt_margo", "margo", "qa");
243 const lib = library(db, owner, ports({ teamList: [{ slug: "qa", name: "QA", can_manage: true }] }));
244 assert.ok((await lib.save(null, notes)).ok);
245 assert.ok((await lib.save(null, { ...notes, name: "triage", description: "Use when a bug comes in." })).ok);
246 assert.ok((await lib.attach("release-notes", "team", "qa")).ok);
247 assert.ok((await lib.attach("triage", "workspace", null)).ok);
248 const rows = await attachedRows(db, WS, "agt_margo", ["qa"]);
249 assert.deepEqual(rows.map((r) => [r.name, r.scope]).sort(), [["release-notes", "team"], ["triage", "workspace"]]);
250 assert.deepEqual(await attachedRows(db, WS, "agt_margo", []).then((r) => r.map((x) => x.name)), ["triage"], "not on the team: not its skill");
251 const shelf = shelfFrom([], rows).skills.filter((s) => s.kind === "library").map((s) => s.name);
252 assert.deepEqual(shelf, ["release-notes", "triage"]);
253 const tab = await lib.agentSkills("margo");
254 assert.ok(tab.ok);
255 assert.deepEqual(
256 tab.value.skills.filter((s) => !s.foundational).map((s) => [s.name, s.via, s.via_label, s.on]),
257 [
258 ["release-notes", "team", "QA", true],
259 ["triage", "workspace", "Every agent", true],
260 ],
261 );
262});
263
264test("uploads: a SKILL.md, a zip of its folder (stored or deflated), and what is refused", async () => {
265 const skillMd = "---\nname: brand-voice\ndescription: >\n Use when writing anything customers read:\n posts, emails and docs.\nlicense: MIT\nmetadata:\n author: acme\n---\n\n# Brand voice\n\nWarm, plain words.\n";
266 const md = await readUpload("SKILL.md", Buffer.from(skillMd).toString("base64"));
267 assert.ok(md.ok);
268 const zipped = storedZip([
269 ["brand-voice/SKILL.md", skillMd],
270 ["brand-voice/resources/words.md", "Use: team. Avoid: synergy."],
271 ["brand-voice/scripts/lint.py", "print('ok')"],
272 ]);
273 const stored = await unzip(zipped);
274 assert.ok(stored.ok);
275 assert.deepEqual(stored.ok && stored.files.map((f) => f.path).sort(), ["SKILL.md", "resources/words.md", "scripts/lint.py"], "the one folder is the skill");
276 const deflated = await deflateZip([
277 { path: "SKILL.md", data: new TextEncoder().encode(skillMd) },
278 { path: "resources/words.md", data: new TextEncoder().encode("x ".repeat(5000)) },
279 { path: "resources/logo.png", data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3]) },
280 ]);
281 const read = await unzip(deflated);
282 assert.ok(read.ok, !read.ok ? read.message : "");
283 assert.equal(read.ok && read.files.find((f) => f.path === "resources/words.md")!.content.length, 10000);
284 assert.equal(read.ok && read.files.find((f) => f.path === "resources/logo.png")!.encoding, "base64");
285
286 const db = fakeD1();
287 const lib = library(db, owner);
288 const imported = await lib.import({ kind: "upload", filename: "brand-voice.zip", data_base64: bytesBase64(zipped) }, false);
289 assert.ok(imported.ok, !imported.ok ? imported.error.message : "");
290 assert.equal(imported.value.skill.description, "Use when writing anything customers read: posts, emails and docs.");
291 assert.equal(imported.value.requires_computer, true, "it has scripts");
292 assert.deepEqual(imported.value.extra, { license: "MIT", metadata: { author: "acme" } });
293 assert.equal(imported.value.skill_md, skillMd, "kept as written");
294 assert.deepEqual(imported.value.files.map((f) => [f.path, f.script]), [["resources/words.md", false], ["scripts/lint.py", true]]);
295 assert.deepEqual(imported.value.skill.origin, { kind: "upload", filename: "brand-voice.zip" });
296 // The same name again: refused, unless as a new version.
297 const again = await lib.import({ kind: "upload", filename: "SKILL.md", data_base64: Buffer.from(skillMd.replace("Warm", "Kind")).toString("base64") }, false);
298 assert.equal(!again.ok && again.error.code, "conflict");
299 const replaced = await lib.import({ kind: "upload", filename: "SKILL.md", data_base64: Buffer.from(skillMd.replace("Warm", "Kind")).toString("base64") }, true);
300 assert.ok(replaced.ok && replaced.value.skill.version === 2);
301 // A new version from a bare SKILL.md keeps nothing else: an upload is the whole folder.
302 assert.deepEqual(replaced.ok && replaced.value.files, []);
303
304 const refuse = async (filename: string, data: Uint8Array, pattern: RegExp) => {
305 const result = await lib.import({ kind: "upload", filename, data_base64: bytesBase64(data) }, false);
306 assert.equal(result.ok, false, filename);
307 assert.match(!result.ok ? result.error.message : "", pattern);
308 };
309 await refuse("notes.pdf", new Uint8Array([1, 2, 3]), /Upload a SKILL\.md, or a zip/);
310 await refuse("big.zip", storedZip([["SKILL.md", skillMd], ["resources/big.txt", "x".repeat(1024 * 1024)]]), /at most 1 MB/);
311 await refuse("nested.zip", storedZip([["a/SKILL.md", skillMd], ["b/x.md", "x"]]), /SKILL\.md belongs at the top of the folder, not in a/);
312 await refuse("escape.zip", storedZip([["SKILL.md", skillMd], ["../x.md", "x"]]), /isn't a path a skill can hold/);
313 await refuse("SKILL.md", new TextEncoder().encode("# No front-matter"), /starts with front-matter/);
314});
315
316test("from a repository: a folder at one commit, read only where the viewer can read", async () => {
317 const db = fakeD1();
318 const repo: Repo = {
319 id: "rep_1",
320 full: "acme/handbook",
321 default_branch: "main",
322 commits: {
323 main: { "skills/oncall/SKILL.md": "---\nname: oncall\ndescription: Use when paged.\n---\n\nAcknowledge first.\n", "skills/oncall/resources/runbook.md": "Steps", "README.md": "x" },
324 v1: { "skills/oncall/SKILL.md": "---\nname: oncall\ndescription: Use when paged.\n---\n\nOld.\n" },
325 },
326 };
327 const lib = library(db, owner, ports({ repos: [repo] }));
328 const made = await lib.import({ kind: "repository", repo: "acme/handbook", path: "/skills/oncall/SKILL.md", ref: "v1" }, false);
329 assert.ok(made.ok, !made.ok ? made.error.message : "");
330 assert.deepEqual(made.value.skill.origin, { kind: "repository", repo: "acme/handbook", path: "skills/oncall", ref: "v1", commit: "v1-sha" });
331 const newer = await lib.import({ kind: "repository", repo: "acme/handbook", path: "skills/oncall" }, true);
332 assert.ok(newer.ok);
333 assert.equal(newer.value.skill.version, 2);
334 assert.deepEqual(newer.value.files.map((f) => f.path), ["resources/runbook.md"]);
335 const hidden = await lib.import({ kind: "repository", repo: "acme/secret", path: "x" }, false);
336 assert.equal(!hidden.ok && hidden.error.code, "not_found");
337 const noRef = await lib.import({ kind: "repository", repo: "acme/handbook", path: "skills/oncall", ref: "nope" }, false);
338 assert.match(!noRef.ok ? noRef.error.message : "", /has no branch, tag or commit called nope/);
339});
340
341test("a linked repository: its .g1t/skills folders become skills, pushes update them, and they're changed only there", async () => {
342 const db = fakeD1();
343 await addAgent(db, "agt_margo", "margo");
344 const repo: Repo = {
345 id: "rep_skills",
346 full: "acme/agents",
347 default_branch: "main",
348 commits: {
349 main: {
350 ".g1t/skills/triage/SKILL.md": "---\nname: triage\ndescription: Use when a bug comes in.\n---\n\nReproduce first.\n",
351 ".g1t/skills/wrong/SKILL.md": "---\nname: other\ndescription: Use when.\n---\n\nx\n",
352 ".g1t/skills/release-notes/SKILL.md": "---\nname: release-notes\ndescription: Use when.\n---\n\nx\n",
353 },
354 },
355 };
356 const p = ports({ repos: [repo] });
357 const lib = library(db, owner, p);
358 assert.ok((await lib.save(null, notes)).ok, "written in the library first");
359 const linked = await lib.setMirror("acme/agents");
360 assert.ok(linked.ok, !linked.ok ? linked.error.message : "");
361 assert.deepEqual(linked.value.changed, ["triage"]);
362 assert.equal(linked.value.problems.length, 2);
363 assert.match(linked.value.problems.join("\n"), /release-notes: the library already has a skill called release-notes that isn't from this repository/);
364 assert.match(linked.value.problems.join("\n"), /wrong: The folder is wrong, but its SKILL\.md is named other/);
365 assert.equal(linked.value.mirror?.commit, "main-sha");
366 const triage = await lib.detail("triage");
367 assert.ok(triage.ok);
368 assert.equal(triage.value.skill.mirrored, true);
369 assert.equal(triage.value.skill.can_edit, false, "changed in the repository, not here");
370 assert.equal((await lib.save("triage", { ...notes, name: "triage" })).ok, false);
371 assert.ok((await lib.attach("triage", "agent", "margo")).ok);
372
373 // A push: a new version, and its attachments follow.
374 repo.commits.main![".g1t/skills/triage/SKILL.md"] = "---\nname: triage\ndescription: Use when a bug comes in.\n---\n\nReproduce first, then label.\n";
375 assert.equal(await onPush(db, p, "rep_skills", at), 1);
376 const pushed = await lib.detail("triage");
377 assert.ok(pushed.ok);
378 assert.equal(pushed.value.skill.version, 2);
379 assert.deepEqual(pushed.value.skill.attachments.map((a) => a.version), [2]);
380 assert.equal(pushed.value.skill.origin.kind, "mirror");
381 assert.equal(await onPush(db, p, "rep_other", at), 0, "nobody follows that one");
382
383 // Gone from the repository: kept, and editable here again.
384 delete repo.commits.main![".g1t/skills/triage/SKILL.md"];
385 const synced = await lib.sync();
386 assert.ok(synced.ok);
387 assert.match(synced.value.problems.join("\n"), /triage is no longer in the repository/);
388 const kept = await lib.detail("triage");
389 assert.ok(kept.ok && !kept.value.skill.mirrored && kept.value.skill.can_edit);
390 // Only owners link one.
391 assert.equal((await library(db, maintainer, maintainerPorts()).setMirror("acme/agents")).ok, false);
392 const unlinked = await lib.setMirror(null);
393 assert.ok(unlinked.ok && unlinked.value.mirror === null);
394});
395
396test("save as skill: a draft from the transcript, never attached until a person publishes it", async () => {
397 const transcript = transcriptText(
398 { title: "Ship the Q3 notes", goal: "Write release notes for Q3.", result: "Done: the doc is linked." },
399 [
400 { kind: "text", by_name: "margo", body: "Reading merged pull requests.", tool: null },
401 { kind: "tool", by_name: "margo", body: '{"repo":"web"}', tool: "recent_activity" },
402 ],
403 );
404 assert.match(transcript, /^<untrusted source="session transcript">\nSession: Ship the Q3 notes/);
405 assert.match(transcript, /- used recent_activity \{"repo":"web"\}/);
406 assert.match(transcript, /Report:\nDone: the doc is linked\.\n<\/untrusted>\n\nWrite the SKILL\.md\.$/);
407 const long = transcriptText({ title: "t", goal: "g", result: null }, Array.from({ length: 200 }, (_, i) => ({ kind: "text", by_name: null, body: `${i} ${"x".repeat(1000)}`, tool: null })));
408 assert.ok(long.length < 62_000 && long.includes("[…]"), "cut in the middle");
409
410 const answer = "```markdown\n---\nname: release-notes\ndescription: Use when someone asks for release notes.\ntools: [recent_activity]\n---\n\n# Release notes\n\n1. Read merged pull requests.\n```";
411 const drafted = draftedSkill(answer);
412 assert.ok(drafted.ok);
413 assert.equal(draftedSkill("Sure! Here it is.").ok, false);
414
415 const db = fakeD1();
416 const lib = library(db, owner);
417 assert.ok((await lib.save(null, notes)).ok, "the name is taken, so the draft gets another");
418 const asMember = library(db, member);
419 const draft = await asMember.saveDraft((drafted as { ok: true; skill: CheckedSkill }).skill, { kind: "session", session_id: "ses_1", agent: "margo", title: "Ship the Q3 notes" });
420 assert.ok(draft.ok, !draft.ok ? draft.error.message : "");
421 assert.equal(draft.value.skill.name, "release-notes-2");
422 assert.match(draft.value.skill_md, /^---\nname: release-notes-2\n/);
423 assert.equal(draft.value.skill.status, "draft");
424 assert.equal(draft.value.skill.can_edit, false, "a member can't publish it");
425 assert.equal(draft.value.skill.can_delete, true, "but can discard their own draft");
426 assert.equal((await lib.attach("release-notes-2", "workspace", null)).ok, false, "a draft can't be attached");
427 assert.deepEqual(await attachedRows(db, WS, "agt_x", []), []);
428 const published = await lib.save("release-notes-2", { name: "q3-notes", description: "Use when someone asks for quarterly release notes.", instructions: "# Notes\n\nRead merged pull requests.", tools: ["recent_activity"] });
429 assert.ok(published.ok, !published.ok ? published.error.message : "");
430 assert.equal(published.value.skill.status, "published");
431 assert.equal(published.value.skill.version, 1, "published in place: a draft has no history");
432 assert.deepEqual(published.value.skill.origin, { kind: "session", session_id: "ses_1", agent: "margo", title: "Ship the Q3 notes" });
433 assert.ok((await lib.attach("q3-notes", "workspace", null)).ok);
434});
435
436test("limits: 100 library skills per agent", async () => {
437 const db = fakeD1();
438 await addAgent(db, "agt_margo", "margo");
439 const lib = library(db, owner);
440 for (let i = 0; i < 101; i++) {
441 const checked = checkSkillFolder([{ path: "SKILL.md", content: `---\nname: s-${i}\ndescription: Use when ${i}.\n---\n\nDo ${i}.\n` }]);
442 assert.ok(checked.ok);
443 const saved = await lib.save(null, { name: `s-${i}`, description: `Use when ${i}.`, instructions: `Do ${i}.` });
444 assert.ok(saved.ok);
445 const attached = await lib.attach(`s-${i}`, i % 2 ? "agent" : "workspace", i % 2 ? "margo" : null);
446 if (i < 100) assert.ok(attached.ok, `${i}: ${!attached.ok ? attached.error.message : ""}`);
447 else assert.match(!attached.ok ? attached.error.message : "", /at most 100 skills from the library/);
448 }
449});