Skip to content
177 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { FolioRef, User } from "@g1t/contracts";
5
6import { FOUNDATIONAL_SKILLS, FOUNDATIONAL_SKILL_IDS, skillTools, skillsOn } from "../../../packages/contracts/src/skills.ts";
7import { Audience, type AudienceInfo } from "./audience.ts";
8import { applyChanges } from "./definition.ts";
9import { systemPrompt } from "./prompt.ts";
10import { skillsSection } from "./skills.ts";
11import { TEMPLATE_IDS } from "./templates.ts";
12import { type FoliosPorts, type ToolPorts, TOOL_NAMES, ToolBox, docBody } from "./tools.ts";
13
14test("the foundational skills name only tools agents have, and say what's coming", () => {
15 assert.deepEqual(FOUNDATIONAL_SKILL_IDS, ["documents", "research", "data", "code", "communication", "files"]);
16 for (const skill of FOUNDATIONAL_SKILLS) {
17 assert.equal(skill.source, "foundational");
18 assert.ok(skill.instructions.length > 100, `${skill.id} has a playbook`);
19 for (const ability of skill.abilities) {
20 for (const tool of ability.tools) assert.ok(TOOL_NAMES.has(tool), `${skill.id}/${ability.id} names ${tool}, which agents have`);
21 if (ability.status === "coming") assert.deepEqual(ability.tools, [], `${skill.id}/${ability.id} is coming, so it uses no tool yet`);
22 assert.ok(ability.note, `${skill.id}/${ability.id} says how or what's missing`);
23 }
24 // Every tool a playbook tells the agent to call is one it can have.
25 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}`);
26 }
27 assert.ok(skillTools(FOUNDATIONAL_SKILLS[0]!).includes("make_file"));
28 const coming = FOUNDATIONAL_SKILLS.flatMap((s) => s.abilities.filter((a) => a.status === "coming").map((a) => a.id));
29 for (const id of ["slides", "search", "browse", "sql", "run", "schedule", "ocr", "images"]) assert.ok(coming.includes(id), `${id} is marked coming`);
30});
31
32test("each skill that is on puts its playbook in the prompt, with what isn't here and what's coming", () => {
33 const all = [...TOOL_NAMES];
34 const section = skillsSection([], all)!;
35 for (const skill of FOUNDATIONAL_SKILLS) assert.match(section, new RegExp(`### ${skill.name}\\n`));
36 assert.match(section, /they never add one/);
37 assert.match(section, /Not yet in g1t: slide decks\./);
38 assert.match(section, /Not yet in g1t: search the web and browse and read pages\./);
39 assert.doesNotMatch(section, /Not available in this conversation/, "every tool is offered");
40 // In a conversation whose people can't all read code: the code abilities say so.
41 const noCode = skillsSection([], all.filter((t) => !["list_repositories", "search_code", "read_file", "recent_activity", "get_pull", "review_pull", "comment", "draft_issue"].includes(t)))!;
42 assert.match(noCode, /### Code[\s\S]*Not available in this conversation \(its tools aren't offered here\): read and explain code, review pull requests and open pull requests\./);
43 // Off: gone from the prompt, the rest stays.
44 const someOff = skillsSection(["communication", "files"], all)!;
45 assert.doesNotMatch(someOff, /### Communication/);
46 assert.doesNotMatch(someOff, /### Files and media/);
47 assert.match(someOff, /### Documents/);
48 assert.equal(skillsSection(FOUNDATIONAL_SKILL_IDS, all), null);
49 assert.equal(skillsOn(["data"]).length, 5);
50 const prompt = systemPrompt({
51 agent: { id: "agt_1", handle: "ship", display_name: "Ship", role: "Release manager", instructions: "Ship.", personality_preset: "crisp", personality: "" },
52 workspace: "acme",
53 channel: { kind: "dm", name: null },
54 asker: { name: "dana", display_name: null, access: null },
55 today: new Date("2026-10-10T00:00:00Z"),
56 skills: section,
57 });
58 assert.ok(prompt.indexOf("## Your skills") > prompt.indexOf("## How to answer"), "skills come after the rules");
59});
60
61test("owners turn skills off by id; unknown ids are refused, and the list reads in the skills' order", () => {
62 const made = applyChanges(null, { handle: "ship", display_name: "Ship", role: "r", instructions: "i" }, TEMPLATE_IDS);
63 assert.ok(made.ok);
64 if (!made.ok) return;
65 assert.deepEqual(made.value.skills_off, []);
66 const off = applyChanges(made.value, { skills_off: ["files", " data ", "files"] }, TEMPLATE_IDS);
67 assert.ok(off.ok);
68 assert.deepEqual(off.ok && off.value.skills_off, ["data", "files"]);
69 const bad = applyChanges(made.value, { skills_off: ["teleport"] }, TEMPLATE_IDS);
70 assert.equal(bad.ok, false);
71 assert.match(!bad.ok ? bad.message : "", /no skill called teleport/);
72 assert.equal(applyChanges(made.value, { skills_off: "data" as unknown as string[] }, TEMPLATE_IDS).ok, false);
73});
74
75// ── make_file, end to end through the tool box ──────────────────────────
76
77const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User;
78
79const basePorts: ToolPorts = {
80 readFile: async () => null,
81 searchCode: async () => [],
82 listIssues: async () => [],
83 getIssue: async () => null,
84 getPull: async () => null,
85 recentPulls: async () => [],
86 searchMessages: async () => [],
87 readThread: async () => null,
88 roster: async () => "",
89 consult: async () => ({ ok: false, message: "no" }),
90};
91
92function fakeFolios(opts: { audienceCanRead?: boolean; canEdit?: boolean } = {}) {
93 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: [] };
94 const ref = (id: string, title: string): FolioRef => ({ id, kind: "doc", title, path: `/acme/-/artifacts/${id}`, icon: null }) as unknown as FolioRef;
95 const folios: FoliosPorts = {
96 spaces: async () => [],
97 recall: async () => [],
98 search: async () => "",
99 read: async (_v, _a, id) => ({
100 ok: true,
101 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,
102 }),
103 stale: async () => "",
104 create: async (_v, input) => {
105 log.created.push({ title: input.title, markdown: input.markdown });
106 return { ok: true, value: ref("fol_new", input.title) };
107 },
108 edit: async (_v, _id, edit) => {
109 log.edits.push(edit.kind === "doc" ? edit.markdown : "");
110 return { ok: true, value: { mode: "applied", version_id: null, folio: ref("fol_new", "x"), summary: "" } };
111 },
112 share: async () => ({ ok: true, value: null }),
113 attach: async (_v, folio, file) => {
114 log.attached.push({ folio, name: file.name, type: file.content_type, bytes: file.bytes.length });
115 return { ok: true, value: { url: "https://g1tusercontent.com/docs-files/abc", name: file.name, bytes: file.bytes.length } };
116 },
117 sendLink: async (_a, link) => {
118 log.links.push(link.path);
119 return true;
120 },
121 };
122 return { folios, log };
123}
124
125const actions = { remember: async () => ({ ok: true, message: "" }), forget: async () => ({ ok: true, message: "" }), draftIssue: async () => ({ ok: true, message: "" }) };
126const context = { agentId: "agt_me", notConsult: [], hops: 0, maxHops: 4 };
127
128async function box(info: AudienceInfo, folios: FoliosPorts) {
129 const people = [person("asker"), person("bea")];
130 const audience = await Audience.build("acme", "asker", {
131 info: async () => info,
132 users: async (ids) => people.filter((u) => ids.includes(u.id)),
133 workspaceRepos: async () => [],
134 readable: async () => [],
135 });
136 return new ToolBox(audience, { ...basePorts, folios }, context, [], actions);
137}
138
139test("make_file makes the PDF, keeps it with a new doc holding its text, links it, and hands back the link", async () => {
140 const { folios, log } = fakeFolios();
141 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
142 assert.ok(tools.definitions().some((t) => t.name === "make_file"));
143 const result = await tools.run("make_file", { format: "pdf", title: "Q3 report", content: "# Q3 report\n\nRevenue grew." });
144 assert.equal(result.outcome, "allowed");
145 assert.match(result.text, /Made Q3 report\.pdf \(1 page, \d+ KB\)/);
146 assert.match(result.text, /https:\/\/g1tusercontent\.com\/docs-files\/abc/);
147 assert.deepEqual(log.created, [{ title: "Q3 report", markdown: "Revenue grew." }], "the doc has its own title, so the heading isn't repeated");
148 assert.equal(log.attached[0]!.type, "application/pdf");
149 assert.equal(log.attached[0]!.folio, "fol_new");
150 assert.match(log.edits[0]!, /^\*\*File:\*\* \[Q3 report\.pdf\]\(https:\/\/g1tusercontent\.com\/docs-files\/abc\)/);
151});
152
153test("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 () => {
154 const readOnly = fakeFolios({ canEdit: false });
155 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, readOnly.folios);
156 const refused = await tools.run("make_file", { format: "csv", title: "Rows", sheets: [{ rows: [["a"], [1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
157 assert.equal(refused.outcome, "refused");
158 assert.match(refused.text, /can.t edit that doc/);
159 assert.equal(readOnly.log.attached.length, 0);
160
161 const hidden = fakeFolios({ audienceCanRead: false });
162 const group = await box({ kind: "private", member_user_ids: ["asker", "bea"], member_count: 2 }, hidden.folios);
163 const sent = await group.run("make_file", { format: "xlsx", title: "Salaries", sheets: [{ rows: [["name", "pay"], ["bea", 1]] }], artifact: "fol_01k7a0b1c2d3e4f5g6h7j8k9mn" });
164 assert.equal(hidden.log.attached.length, 1, `kept with the doc: ${sent.text}`);
165 assert.doesNotMatch(sent.text, /docs-files/, "the link isn't given here");
166 assert.deepEqual(hidden.log.links, ["/acme/-/artifacts/fol_01k7a0b1c2d3e4f5g6h7j8k9mn"], "the person who asked gets it directly");
167});
168
169test("a spreadsheet's doc shows its rows; a format it can't write is refused with what to do instead", async () => {
170 assert.match(docBody("xlsx", "Sales", { sheets: [{ name: "S", rows: [["a", "b"], [1, 2]] }] }), /^\| a \| b \|/);
171 assert.equal(docBody("pdf", "T", { content: "# T" }), "The file is attached below.");
172 const { folios } = fakeFolios();
173 const tools = await box({ kind: "dm", member_user_ids: ["asker"], member_count: 1 }, folios);
174 const deck = await tools.run("make_file", { format: "pptx", title: "Deck" });
175 assert.equal(deck.outcome, "refused");
176 assert.match(deck.text, /offer a PDF or a doc instead/);
177});