Skip to content
189 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { User } from "@g1t/contracts";
5
6import { canManage, canSee } from "./access.ts";
7import { DEFAULT_AUTONOMY, DEFAULT_CAPACITY, applyChanges, readJson } from "./definition.ts";
8import { TEMPLATES, TEMPLATE_IDS } from "./templates.ts";
9
10const input = { handle: "Ship", display_name: " Ship ", role: "Release manager", instructions: "Cut releases." };
11
12test("a new agent gets safe defaults", () => {
13 const made = applyChanges(null, input, TEMPLATE_IDS);
14 assert.ok(made.ok);
15 if (!made.ok) return;
16 assert.equal(made.value.handle, "ship");
17 assert.equal(made.value.display_name, "Ship");
18 assert.equal(made.value.personality_preset, "crisp");
19 assert.equal(made.value.capacity, DEFAULT_CAPACITY);
20 assert.deepEqual(made.value.autonomy, DEFAULT_AUTONOMY);
21 assert.equal(made.value.autonomy.merge, "approval");
22 assert.deepEqual(made.value.routing, { floor: null, ceiling: null, providers: [], pinned: null });
23});
24
25test("a new agent needs a handle, a name, a role and instructions", () => {
26 for (const missing of ["handle", "display_name", "role", "instructions"] as const) {
27 const made = applyChanges(null, { ...input, [missing]: "" }, TEMPLATE_IDS);
28 assert.equal(made.ok, false, missing);
29 }
30 assert.equal(applyChanges(null, { ...input, handle: "g1t" }, TEMPLATE_IDS).ok, false);
31});
32
33test("a change merges onto the definition, and is checked", () => {
34 const made = applyChanges(null, input, TEMPLATE_IDS);
35 assert.ok(made.ok);
36 if (!made.ok) return;
37 const changed = applyChanges(made.value, { routing: { floor: "large" }, budget: { monthly_micros: 5_000_000 }, autonomy: { merge: "never" } }, TEMPLATE_IDS);
38 assert.ok(changed.ok);
39 if (!changed.ok) return;
40 assert.equal(changed.value.handle, "ship", "untouched fields stay");
41 assert.equal(changed.value.routing.floor, "large");
42 assert.equal(changed.value.routing.ceiling, null);
43 assert.equal(changed.value.budget.monthly_micros, 5_000_000);
44 assert.equal(changed.value.budget.daily_micros, null);
45 assert.equal(changed.value.autonomy.merge, "never");
46 assert.equal(changed.value.autonomy.open_pull_requests, "alone");
47
48 const bad = (changes: object) => applyChanges(made.value, changes, TEMPLATE_IDS).ok;
49 assert.equal(bad({ routing: { floor: "frontier", ceiling: "small" } }), false, "floor above ceiling");
50 assert.equal(bad({ routing: { floor: "huge" } }), false);
51 assert.equal(bad({ routing: { pinned: "no-slash" } }), false);
52 assert.equal(bad({ budget: { monthly_micros: -1 } }), false);
53 assert.equal(bad({ budget: { daily_micros: 1.5 } }), false);
54 assert.equal(bad({ autonomy: { deploy_production: "alone" } }), false, "production deploys always ask");
55 assert.equal(bad({ capacity: 0 }), false);
56 assert.equal(bad({ capacity: 11 }), false);
57 assert.equal(bad({ personality_preset: "pirate" }), false);
58 assert.equal(bad({ template: "nope" }), false);
59 assert.equal(bad({ template: "qa" }), true);
60 for (const providers of [[], ["g1t"], ["workspace"], ["int_1"], ["g1t", "workspace"]]) assert.equal(bad({ routing: { providers } }), true, JSON.stringify(providers));
61 assert.equal(bad({ routing: { providers: [""] } }), false);
62 assert.equal(bad({ instructions: "x".repeat(8001) }), false);
63});
64
65test("a stored JSON column that cannot be read is the default", () => {
66 assert.deepEqual(readJson("{bad", { a: 1 }), { a: 1 });
67 assert.deepEqual(readJson('{"b":2}', { a: 1 }), { a: 1, b: 2 });
68 assert.deepEqual(readJson(null, { a: 1 }), { a: 1 });
69});
70
71test("every template is a valid definition with the routing its job needs", () => {
72 assert.deepEqual(TEMPLATE_IDS, ["engineering", "qa", "operations", "docs", "product", "support", "sales"]);
73 for (const template of TEMPLATES) {
74 const made = applyChanges(null, { ...template, template: template.id }, TEMPLATE_IDS);
75 assert.ok(made.ok, template.id);
76 }
77 const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t]));
78 assert.equal(byId.qa.routing.floor, "large", "careful review never runs below large");
79 assert.equal(byId.product.routing.ceiling, "large", "intake never runs above it");
80 assert.equal(new Set(TEMPLATES.map((t) => t.handle)).size, TEMPLATES.length, "handles are distinct");
81});
82
83test("members see a workspace's agents; owners change them", () => {
84 const person = (role: "owner" | "member"): User => ({ id: "u", username: "u", workspaces: [{ slug: "acme", role }] }) as User;
85 assert.ok(canSee(person("member"), "ACME"));
86 assert.ok(!canManage(person("member"), "acme"));
87 assert.ok(canManage(person("owner"), "acme"));
88 assert.ok(!canSee(person("owner"), "other"));
89 assert.ok(!canSee(null, "acme"));
90 const token = { id: "w", username: "acme", kind: "workspace" } as User;
91 assert.ok(canManage(token, "acme"), "the workspace's own token");
92 assert.ok(!canSee(token, "other"));
93});
94
95test("templates are named like teammates, with more names to shuffle, each a valid handle", async () => {
96 const { checkHandle } = await import("./handle.ts");
97 const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t]));
98 assert.equal(byId.qa.display_name, "Margo");
99 assert.equal(byId.qa.handle, "margo");
100 for (const template of TEMPLATES) {
101 assert.ok(!/reviewer|planner|triage|implementer|documenter|on-call|release/i.test(template.display_name), `${template.id} has a name, not a job title`);
102 assert.ok(template.name_ideas.length >= 6 && template.name_ideas.length <= 10, template.id);
103 assert.ok(template.name_ideas.includes(template.display_name), `${template.id}'s own name is among its ideas`);
104 for (const name of template.name_ideas) assert.ok(checkHandle(name.toLowerCase()).ok, `${name} works as a handle`);
105 assert.equal(new Set(template.name_ideas).size, template.name_ideas.length, `${template.id} repeats no name`);
106 }
107});
108
109test("an agent's avatar seed starts as its handle, can be changed, and stays through a rename", () => {
110 const made = applyChanges(null, input, TEMPLATE_IDS);
111 assert.ok(made.ok);
112 if (!made.ok) return;
113 assert.equal(made.value.avatar_seed, "ship");
114 const renamed = applyChanges(made.value, { handle: "skipper" }, TEMPLATE_IDS);
115 assert.ok(renamed.ok);
116 if (renamed.ok) assert.equal(renamed.value.avatar_seed, "ship", "a new name keeps the face");
117 const reseeded = applyChanges(made.value, { avatar_seed: "ship-2" }, TEMPLATE_IDS);
118 assert.ok(reseeded.ok);
119 if (reseeded.ok) assert.equal(reseeded.value.avatar_seed, "ship-2");
120 assert.equal(applyChanges(made.value, { avatar_seed: "x".repeat(65) }, TEMPLATE_IDS).ok, false);
121 const chosen = applyChanges(null, { ...input, avatar_seed: "blob" }, TEMPLATE_IDS);
122 assert.ok(chosen.ok && chosen.value.avatar_seed === "blob");
123});
124
125test("an agent is hired into a role: title, team or department, responsibilities", async () => {
126 const { roleOf } = await import("./definition.ts");
127 const made = applyChanges(null, { handle: "margo", display_name: "Margo", title: "QA Engineer", department: "QA", instructions: "Test things.", responsibilities: ["Review pull requests", "Chase flaky checks"] }, TEMPLATE_IDS);
128 assert.ok(made.ok);
129 if (!made.ok) return;
130 assert.equal(made.value.role, "QA Engineer, QA", "the one-line role is made from the title and department");
131 const onTeam = applyChanges(made.value, { team: "QA-Core" }, TEMPLATE_IDS);
132 assert.ok(onTeam.ok && onTeam.value.role === "QA Engineer on the qa-core team", "a made role follows the title and team");
133 const written = applyChanges(made.value, { role: "Keeps us honest" }, TEMPLATE_IDS);
134 assert.ok(written.ok);
135 if (!written.ok) return;
136 const kept = applyChanges(written.value, { title: "QA Lead" }, TEMPLATE_IDS);
137 assert.ok(kept.ok && kept.value.role === "Keeps us honest", "a written role is kept");
138 assert.equal(roleOf({ title: "", team: null, department: "QA" }), "");
139 assert.equal(applyChanges(null, { handle: "x1", display_name: "X", instructions: "y" }, TEMPLATE_IDS).ok, false, "a title or a role is needed");
140 const bad = (changes: object) => applyChanges(made.value, changes, TEMPLATE_IDS).ok;
141 assert.equal(bad({ responsibilities: ["Only one"] }), false, "2 to 8 duties");
142 assert.equal(bad({ responsibilities: Array.from({ length: 9 }, (_, i) => `Duty ${i}`) }), false);
143 assert.equal(bad({ responsibilities: [] }), true, "or none yet");
144 assert.equal(bad({ team: "not a slug!" }), false);
145 assert.equal(bad({ team: null }), true);
146});
147
148test("subagents: named, described, at most 8, never wider than their agent", () => {
149 const base = applyChanges(null, { ...input, routing: { floor: "large", ceiling: "large" } }, TEMPLATE_IDS);
150 assert.ok(base.ok);
151 if (!base.ok) return;
152 const helper = { name: "flake-hunter", description: "Bisects flaky tests", instructions: "Find the cause.", routing: { floor: "small" as const, ceiling: "frontier" as const }, max_parallel: 3 };
153 const withHelper = applyChanges(base.value, { subagents: [helper] }, TEMPLATE_IDS);
154 assert.ok(withHelper.ok);
155 if (!withHelper.ok) return;
156 assert.deepEqual(withHelper.value.subagents[0].routing, { floor: "large", ceiling: "large" }, "held within the agent's limits");
157 const loosened = applyChanges(withHelper.value, { routing: { floor: null, ceiling: null } }, TEMPLATE_IDS);
158 assert.ok(loosened.ok && loosened.value.subagents[0].routing.floor === "large", "loosening the agent later does not widen what was stored");
159 const bad = (subagents: unknown) => applyChanges(base.value, { subagents: subagents as never }, TEMPLATE_IDS).ok;
160 assert.equal(bad([{ ...helper, name: "Flake Hunter" }]), false);
161 assert.equal(bad([helper, helper]), false, "names are unique");
162 assert.equal(bad([{ ...helper, description: "" }]), false);
163 assert.equal(bad([{ ...helper, max_parallel: 9 }]), false);
164 assert.equal(bad(Array.from({ length: 9 }, (_, i) => ({ ...helper, name: `helper-${i}` }))), false, "at most 8");
165 assert.equal(bad([{ ...helper, routing: { floor: "frontier", ceiling: "small" } }]), false);
166});
167
168test("agents face the workspace's own people; customer-facing ones aren't available yet", () => {
169 const made = applyChanges(null, input, TEMPLATE_IDS);
170 assert.ok(made.ok && made.value.faces === "internal");
171 const customers = applyChanges(null, { ...input, faces: "customers" }, TEMPLATE_IDS);
172 assert.ok(!customers.ok && customers.message === "Customer-facing agents aren't available yet.");
173});
174
175test("role templates by department, each with a title, duties and a subagent or two; David is back office", () => {
176 const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t]));
177 for (const template of TEMPLATES) {
178 assert.ok(template.title && template.department, template.id);
179 assert.ok(template.responsibilities.length >= 2 && template.responsibilities.length <= 8, template.id);
180 assert.ok(template.subagents.length >= 1 && template.subagents.length <= 2, template.id);
181 }
182 assert.equal(byId.sales.display_name, "David");
183 assert.equal(byId.sales.title, "Sales Operations");
184 assert.equal(byId.sales.department, "Sales");
185 assert.match(byId.sales.instructions, /never contact a customer/i);
186 assert.match(byId.sales.instructions, /never promise roadmap/i);
187 assert.match(byId.sales.instructions, /audience/);
188 assert.ok(!TEMPLATE_IDS.includes("planner"), "planning is g1t's own job");
189});