Skip to content
268 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { FolioRef, User } from "@g1t/contracts";
5
6import { AGENT_TOOL_NAMES, RESERVED_SKILL_NAMES, SKILLS_PER_AGENT_MAX } from "../../../packages/contracts/src/skill-format.ts";
7import { FOUNDATIONAL_SKILLS, FOUNDATIONAL_SKILL_IDS, skillTools, skillsOn } from "../../../packages/contracts/src/skills.ts";
8import { Audience, type AudienceInfo } from "./audience.ts";
9import { applyChanges } from "./definition.ts";
10import { systemPrompt } from "./prompt.ts";
11import { type AttachedRow, shelfFrom, skillsSection, teamSlugs } from "./skills.ts";
12import { TEMPLATE_IDS } from "./templates.ts";
13import { type FoliosPorts, type ToolPorts, TOOL_NAMES, ToolBox, docBody } from "./tools.ts";
14
15test("the foundational skills name only tools agents have, and say what's coming", () => {
16 assert.deepEqual(FOUNDATIONAL_SKILL_IDS, ["documents", "research", "data", "code", "communication", "files"]);
17 for (const skill of FOUNDATIONAL_SKILLS) {
18 assert.equal(skill.source, "foundational");
19 assert.ok(skill.instructions.length > 100, `${skill.id} has a playbook`);
20 for (const ability of skill.abilities) {
21 for (const tool of ability.tools) assert.ok(TOOL_NAMES.has(tool), `${skill.id}/${ability.id} names ${tool}, which agents have`);
22 if (ability.status === "coming") assert.deepEqual(ability.tools, [], `${skill.id}/${ability.id} is coming, so it uses no tool yet`);
23 assert.ok(ability.note, `${skill.id}/${ability.id} says how or what's missing`);
24 }
25 // Every tool a playbook tells the agent to call is one it can have.
26 for (const [, tool] of skill.instructions.matchAll(/\b([a-z]+_[a-z_]+)\b/g)) assert.ok(TOOL_NAMES.has(tool!), `${skill.id}'s playbook names ${tool}`);
27 }
28 assert.ok(skillTools(FOUNDATIONAL_SKILLS[0]!).includes("make_file"));
29 const coming = FOUNDATIONAL_SKILLS.flatMap((s) => s.abilities.filter((a) => a.status === "coming").map((a) => a.id));
30 for (const id of ["slides", "search", "browse", "sql", "run", "schedule", "ocr", "images"]) assert.ok(coming.includes(id), `${id} is marked coming`);
31});
32
33test("the tools a skill may name are exactly the tools agents have", () => {
34 assert.deepEqual([...AGENT_TOOL_NAMES].sort(), [...TOOL_NAMES].sort());
35 assert.deepEqual(RESERVED_SKILL_NAMES, FOUNDATIONAL_SKILL_IDS);
36});
37
38const library = (over: Partial<AttachedRow> = {}): AttachedRow => ({
39 skill_id: "skl_01k7a0b1c2d3e4f5g6h7j8k9mn",
40 name: "release-notes",
41 description: "Use when someone asks for release notes.",
42 version: 3,
43 tools: '["recent_activity","create_artifact","teleport_tool"]',
44 requires_computer: 0,
45 scope: "workspace",
46 attached_at: "2026-10-01T00:00:00Z",
47 ...over,
48});
49
50test("the prompt names each skill and when to use it; the playbooks stay out until read", () => {
51 const all = [...TOOL_NAMES];
52 const { skills } = shelfFrom([], [library()]);
53 const section = skillsSection(skills, all)!;
54 for (const skill of FOUNDATIONAL_SKILLS) assert.match(section, new RegExp(`^- ${skill.id}: ${skill.when.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
55 assert.match(section, /^- release-notes: Use when someone asks for release notes\.$/m);
56 assert.match(section, /call use_skill with its name before you start/);
57 assert.match(section, /never add one/);
58 for (const skill of FOUNDATIONAL_SKILLS) assert.ok(!section.includes(skill.instructions), `${skill.id}'s playbook isn't in the prompt`);
59 // Without use_skill (no tools at all), nothing is listed: no skill could be followed.
60 assert.equal(skillsSection(skills, all.filter((t) => t !== "use_skill")), null);
61 // Off: gone from the list, the rest stays; all off and none attached, no section.
62 const someOff = skillsSection(shelfFrom(["communication", "files", "skl_01k7a0b1c2d3e4f5g6h7j8k9mn"], [library()]).skills, all)!;
63 assert.doesNotMatch(someOff, /^- communication:/m);
64 assert.doesNotMatch(someOff, /^- files:/m);
65 assert.doesNotMatch(someOff, /release-notes/);
66 assert.match(someOff, /^- documents:/m);
67 assert.equal(skillsSection(shelfFrom(FOUNDATIONAL_SKILL_IDS, []).skills, all), null);
68 assert.equal(skillsOn(["data"]).length, 5);
69 // A skill that needs a computer is marked.
70 assert.match(skillsSection(shelfFrom([], [library({ requires_computer: 1 })]).skills, all)!, /release-notes: .* Needs a computer of its own \(not available yet\)\./);
71 const prompt = systemPrompt({
72 agent: { id: "agt_1", handle: "ship", display_name: "Ship", role: "Release manager", instructions: "Ship.", personality_preset: "crisp", personality: "" },
73 workspace: "acme",
74 channel: { kind: "dm", name: null },
75 asker: { name: "dana", display_name: null, access: null },
76 today: new Date("2026-10-10T00:00:00Z"),
77 skills: section,
78 });
79 assert.ok(prompt.indexOf("## Your skills") > prompt.indexOf("## How to answer"), "skills come after the rules");
80});
81
82test("owners turn skills off by id; unknown ids are refused, and the list reads in the skills' order", () => {
83 const made = applyChanges(null, { handle: "ship", display_name: "Ship", role: "r", instructions: "i" }, TEMPLATE_IDS);
84 assert.ok(made.ok);
85 if (!made.ok) return;
86 assert.deepEqual(made.value.skills_off, []);
87 const off = applyChanges(made.value, { skills_off: ["files", " data ", "files"] }, TEMPLATE_IDS);
88 assert.ok(off.ok);
89 assert.deepEqual(off.ok && off.value.skills_off, ["data", "files"]);
90 // Library skills by their ids, after the foundational ones.
91 const lib = applyChanges(made.value, { skills_off: ["skl_01k7a0b1c2d3e4f5g6h7j8k9mn", "files"] }, TEMPLATE_IDS);
92 assert.deepEqual(lib.ok && lib.value.skills_off, ["files", "skl_01k7a0b1c2d3e4f5g6h7j8k9mn"]);
93 const bad = applyChanges(made.value, { skills_off: ["teleport"] }, TEMPLATE_IDS);
94 assert.equal(bad.ok, false);
95 assert.match(!bad.ok ? bad.message : "", /no skill called teleport/);
96 assert.equal(applyChanges(made.value, { skills_off: ["skl_short"] }, TEMPLATE_IDS).ok, false);
97 assert.equal(applyChanges(made.value, { skills_off: "data" as unknown as string[] }, TEMPLATE_IDS).ok, false);
98});
99
100test("a library skill reaches an agent once, at the version where it is attached closest, and at most 100 do", () => {
101 const id = (n: number) => `skl_${String(n).padStart(26, "0")}`;
102 const { skills } = shelfFrom(
103 [],
104 [
105 library({ scope: "workspace", version: 1 }),
106 library({ scope: "agent", version: 3, attached_at: "2026-10-05T00:00:00Z" }),
107 library({ scope: "team", version: 2 }),
108 ],
109 );
110 const lib = skills.filter((s) => s.kind === "library");
111 assert.equal(lib.length, 1);
112 assert.equal(lib[0]!.kind === "library" && lib[0]!.version, 3, "attached to the agent itself wins");
113 assert.equal(lib[0]!.kind === "library" && lib[0]!.via, "agent");
114 const many = Array.from({ length: SKILLS_PER_AGENT_MAX + 5 }, (_, n) => library({ skill_id: id(n), name: `s-${String(n).padStart(3, "0")}`, attached_at: `2026-10-01T00:00:${String(n % 60).padStart(2, "0")}Z` }));
115 const capped = shelfFrom([], many);
116 assert.equal(capped.skills.filter((s) => s.kind === "library").length, SKILLS_PER_AGENT_MAX);
117 assert.equal(capped.over, 5);
118 assert.deepEqual(teamSlugs({ teams: [{ slug: "qa" }, { slug: "web" }] as never, agents: [], presence: [] }, "qa"), ["qa", "web"]);
119 assert.deepEqual(teamSlugs(null, "qa"), ["qa"], "its home team when teams couldn't be read");
120});
121
122test("use_skill reads a skill the agent has: the playbook with what isn't here, files, and scripts never run", async () => {
123 const { folios } = fakeFolios();
124 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
125 assert.ok(!tools.definitions().some((t) => t.name === "use_skill"), "no skills, no use_skill");
126 const stored = {
127 skill_md: "---\nname: release-notes\ndescription: Use when someone asks for release notes.\ntools: [recent_activity, create_artifact]\n---\n\n# Release notes\n\nGroup changes by area.\n",
128 files: [
129 { path: "resources/template.md", content: "## Added\n\n## Fixed\n", encoding: "utf8" as const },
130 { path: "scripts/collect.py", content: "print('hi')\n", encoding: "utf8" as const },
131 { path: "resources/logo.png", content: "iVBORw0KGgo=", encoding: "base64" as const },
132 ],
133 };
134 const reads: string[] = [];
135 const shelf = shelfFrom(["files"], [library({ tools: '["recent_activity","create_artifact"]', requires_computer: 1 })]).skills;
136 tools.useShelf(shelf, async (skillId, version) => {
137 reads.push(`${skillId}@${version}`);
138 return stored;
139 });
140 assert.ok(tools.definitions().some((t) => t.name === "use_skill"));
141 const documents = await tools.run("use_skill", { name: "documents" });
142 assert.equal(documents.outcome, "allowed");
143 assert.match(documents.text, /When someone asks for a document, give them the document/);
144 assert.match(documents.text, /Not yet in g1t: slide decks\./);
145 assert.doesNotMatch(documents.text, /Not available in this conversation/, "every tool it uses is offered here");
146 const off = await tools.run("use_skill", { name: "files" });
147 assert.equal(off.outcome, "refused", "a skill that is off isn't the agent's");
148 const notes = await tools.run("use_skill", { name: "release-notes" });
149 assert.equal(notes.outcome, "allowed");
150 assert.deepEqual(reads, ["skl_01k7a0b1c2d3e4f5g6h7j8k9mn@3"], "the pinned version is read");
151 assert.match(notes.text, /^# release-notes \(your workspace's skill, version 3\)\n\n# Release notes\n\nGroup changes by area\./);
152 assert.doesNotMatch(notes.text, /^name:/m, "front-matter isn't repeated");
153 assert.match(notes.text, /needs a computer of its own, which agents don't have yet: its scripts can't run/);
154 assert.match(notes.text, /Its files, which you can read with use_skill and file: resources\/template\.md \(19 B\), scripts\/collect\.py \(12 B\), resources\/logo\.png \(8 B\)\./);
155 const template = await tools.run("use_skill", { name: "release-notes", file: "resources/template.md" });
156 assert.match(template.text, /## Added/);
157 const script = await tools.run("use_skill", { name: "release-notes", file: "scripts/collect.py" });
158 assert.match(script.text, /This is a script: it needs a computer of its own/);
159 const image = await tools.run("use_skill", { name: "release-notes", file: "resources/logo.png" });
160 assert.match(image.text, /isn't text/);
161 const missing = await tools.run("use_skill", { name: "teleport" });
162 assert.equal(missing.outcome, "refused");
163 assert.match(missing.text, /no skill called teleport\. Your skills: documents, research, data, code, communication, release-notes\./);
164});
165
166// ── make_file, end to end through the tool box ──────────────────────────
167
168const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User;
169
170const basePorts: ToolPorts = {
171 readFile: async () => null,
172 searchCode: async () => [],
173 listIssues: async () => [],
174 getIssue: async () => null,
175 getPull: async () => null,
176 recentPulls: async () => [],
177 searchMessages: async () => [],
178 readThread: async () => null,
179 roster: async () => "",
180 consult: async () => ({ ok: false, message: "no" }),
181};
182
183function fakeFolios(opts: { audienceCanRead?: boolean; canEdit?: boolean } = {}) {
184 const log: { created: { title: string; markdown: string | null }[]; attached: { folio: string; name: string; type: string; bytes: number }[]; edits: string[]; links: string[] } = { created: [], attached: [], edits: [], links: [] };
185 const ref = (id: string, title: string): FolioRef => ({ id, kind: "doc", title, path: `/acme/-/artifacts/${id}`, icon: null }) as unknown as FolioRef;
186 const folios: FoliosPorts = {
187 spaces: async () => [],
188 recall: async () => [],
189 search: async () => "",
190 read: async (_v, _a, id) => ({
191 ok: true,
192 value: { folio: { ...ref(id, "Existing"), edited_at: "2026-10-10" }, space: null, content: "", can: { read: true, suggest: true, edit: opts.canEdit ?? true }, audience_can_read: opts.audienceCanRead ?? true } as never,
193 }),
194 stale: async () => "",
195 create: async (_v, input) => {
196 log.created.push({ title: input.title, markdown: input.markdown });
197 return { ok: true, value: ref("fol_new", input.title) };
198 },
199 edit: async (_v, _id, edit) => {
200 log.edits.push(edit.kind === "doc" ? edit.markdown : "");
201 return { ok: true, value: { mode: "applied", version_id: null, folio: ref("fol_new", "x"), summary: "" } };
202 },
203 share: async () => ({ ok: true, value: null }),
204 attach: async (_v, folio, file) => {
205 log.attached.push({ folio, name: file.name, type: file.content_type, bytes: file.bytes.length });
206 return { ok: true, value: { url: "https://g1tusercontent.com/docs-files/abc", name: file.name, bytes: file.bytes.length } };
207 },
208 sendLink: async (_a, link) => {
209 log.links.push(link.path);
210 return true;
211 },
212 };
213 return { folios, log };
214}
215
216const actions = { remember: async () => ({ ok: true, message: "" }), forget: async () => ({ ok: true, message: "" }), draftIssue: async () => ({ ok: true, message: "" }) };
217const context = { agentId: "agt_me", notConsult: [], hops: 0, maxHops: 4 };
218
219async function box(info: AudienceInfo, folios: FoliosPorts) {
220 const people = [person("asker"), person("bea")];
221 const audience = await Audience.build("acme", "asker", {
222 info: async () => info,
223 users: async (ids) => people.filter((u) => ids.includes(u.id)),
224 workspaceRepos: async () => [],
225 readable: async () => [],
226 });
227 return new ToolBox(audience, { ...basePorts, folios }, context, [], actions);
228}
229
230test("make_file makes the PDF, keeps it with a new doc holding its text, links it, and hands back the link", async () => {
231 const { folios, log } = fakeFolios();
232 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
233 assert.ok(tools.definitions().some((t) => t.name === "make_file"));
234 const result = await tools.run("make_file", { format: "pdf", title: "Q3 report", content: "# Q3 report\n\nRevenue grew." });
235 assert.equal(result.outcome, "allowed");
236 assert.match(result.text, /Made Q3 report\.pdf \(1 page, \d+ KB\)/);
237 assert.match(result.text, /https:\/\/g1tusercontent\.com\/docs-files\/abc/);
238 assert.deepEqual(log.created, [{ title: "Q3 report", markdown: "Revenue grew." }], "the doc has its own title, so the heading isn't repeated");
239 assert.equal(log.attached[0]!.type, "application/pdf");
240 assert.equal(log.attached[0]!.folio, "fol_new");
241 assert.match(log.edits[0]!, /^\*\*File:\*\* \[Q3 report\.pdf\]\(https:\/\/g1tusercontent\.com\/docs-files\/abc\)/);
242});
243
244test("make_file attaches to a doc only where it may edit, and keeps a doc others here can't read out of the conversation", async () => {
245 const readOnly = fakeFolios({ canEdit: false });
246 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, readOnly.folios);
247 const refused = await tools.run("make_file", { format: "csv", title: "Rows", sheets: [{ rows: [["a"], [1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
248 assert.equal(refused.outcome, "refused");
249 assert.match(refused.text, /can.t edit that doc/);
250 assert.equal(readOnly.log.attached.length, 0);
251
252 const hidden = fakeFolios({ audienceCanRead: false });
253 const group = await box({ kind: "private", member_user_ids: ["asker", "bea"], member_count: 2 }, hidden.folios);
254 const sent = await group.run("make_file", { format: "xlsx", title: "Salaries", sheets: [{ rows: [["name", "pay"], ["bea", 1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
255 assert.equal(hidden.log.attached.length, 1, `kept with the doc: ${sent.text}`);
256 assert.doesNotMatch(sent.text, /docs-files/, "the link isn't given here");
257 assert.deepEqual(hidden.log.links, ["/acme/-/artifacts/fol_01k7a0b1c2d3e4f5g6h7j8k9mn"], "the person who asked gets it directly");
258});
259
260test("a spreadsheet's doc shows its rows; a format it can't write is refused with what to do instead", async () => {
261 assert.match(docBody("xlsx", "Sales", { sheets: [{ name: "S", rows: [["a", "b"], [1, 2]] }] }), /^\| a \| b \|/);
262 assert.equal(docBody("pdf", "T", { content: "# T" }), "The file is attached below.");
263 const { folios } = fakeFolios();
264 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
265 const deck = await tools.run("make_file", { format: "pptx", title: "Deck" });
266 assert.equal(deck.outcome, "refused");
267 assert.match(deck.text, /offer a PDF or a doc instead/);
268});