| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { User } from "@g1t/contracts"; |
| 5 | |
| 6 | import { canManage, canSee } from "./access.ts"; |
| 7 | import { DEFAULT_AUTONOMY, DEFAULT_CAPACITY, applyChanges, readJson } from "./definition.ts"; |
| 8 | import { TEMPLATES, TEMPLATE_IDS } from "./templates.ts"; |
| 9 | |
| 10 | const input = { handle: "Ship", display_name: " Ship ", role: "Release manager", instructions: "Cut releases." }; |
| 11 | |
| 12 | test("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 | |
| 25 | test("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 | |
| 33 | test("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 | |
| 65 | test("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 | |
| 71 | test("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 | |
| 83 | test("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 | |
| 95 | test("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 | |
| 109 | test("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 | |
| 125 | test("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 | |
| 148 | test("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 | |
| 168 | test("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 | |
| 175 | test("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 | }); |