Skip to content
268 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { FolioRef, User } from "@g1t/contracts";
5
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.6import { AGENT_TOOL_NAMES, RESERVED_SKILL_NAMES, SKILLS_PER_AGENT_MAX } from "../../../packages/contracts/src/skill-format.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.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";
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.11import { type AttachedRow, shelfFrom, skillsSection, teamSlugs } from "./skills.ts";
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.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
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.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", () => {
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.51 const all = [...TOOL_NAMES];
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.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);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.68 assert.equal(skillsOn(["data"]).length, 5);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.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\)\./);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.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"]);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.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"]);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.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/);
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.96 assert.equal(applyChanges(made.value, { skills_off: ["skl_short"] }, TEMPLATE_IDS).ok, false);
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.97 assert.equal(applyChanges(made.value, { skills_off: "data" as unknown as string[] }, TEMPLATE_IDS).ok, false);
98});
99
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.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
Every agent starts with g1t's foundational skills, playbooks for documents, research, data, code, communication, and files and media, each using only tools the agent already has and saying plainly what isn't possible yet: agents make PDFs, Word documents, spreadsheets, CSV and Markdown files with make_file, kept with a doc in Artifacts and served from g1tusercontent.com; an agent's Skills tab shows what each skill can do and with which tools, and owners turn a skill off per agent; the agent skills guide says how.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});