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. | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import 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. | 6 | import { 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. | 7 | import { FOUNDATIONAL_SKILLS, FOUNDATIONAL_SKILL_IDS, skillTools, skillsOn } from "../../../packages/contracts/src/skills.ts"; |
| 8 | import { Audience, type AudienceInfo } from "./audience.ts"; | |
| 9 | import { applyChanges } from "./definition.ts"; | |
| 10 | import { 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. | 11 | import { 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. | 12 | import { TEMPLATE_IDS } from "./templates.ts"; |
| 13 | import { type FoliosPorts, type ToolPorts, TOOL_NAMES, ToolBox, docBody } from "./tools.ts"; | |
| 14 | ||
| 15 | test("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. | 33 | test("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 | ||
| 38 | const 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 | ||
| 50 | test("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 | ||
| 82 | test("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. | 100 | test("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 | ||
| 122 | test("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 | ||
| 168 | const person = (id: string): User => ({ id, username: id, workspaces: [{ slug: "acme", role: "member" }] }) as User; | |
| 169 | ||
| 170 | const 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 | ||
| 183 | function 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 | ||
| 216 | const actions = { remember: async () => ({ ok: true, message: "" }), forget: async () => ({ ok: true, message: "" }), draftIssue: async () => ({ ok: true, message: "" }) }; | |
| 217 | const context = { agentId: "agt_me", notConsult: [], hops: 0, maxHops: 4 }; | |
| 218 | ||
| 219 | async 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 | ||
| 230 | test("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 | ||
| 244 | test("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 | ||
| 260 | test("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 | }); |