Skip to content
101 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { readFileSync, readdirSync } from "node:fs";
3import { DatabaseSync } from "node:sqlite";
4import { test } from "node:test";
5
6import type { AdoptedAgentTeam } from "@g1t/contracts";
7
8import { claimsByWorkspace, moveAgentTeams } from "./team-move.ts";
9
10/** D1 over node's SQLite with the service's migrations. */
11function fakeD1(): D1Database {
12 const db = new DatabaseSync(":memory:");
13 const dir = new URL("../migrations/", import.meta.url);
14 for (const file of readdirSync(dir).sort()) db.exec(readFileSync(new URL(file, dir), "utf8"));
15 const statement = (sql: string, params: unknown[] = []): any => ({
16 sql,
17 params,
18 bind: (...values: unknown[]) => statement(sql, values),
19 first: async () => (db.prepare(sql).get(...(params as never[])) as unknown) ?? null,
20 run: async () => ({ meta: { changes: Number(db.prepare(sql).run(...(params as never[])).changes) } }),
21 all: async () => ({ results: db.prepare(sql).all(...(params as never[])) }),
22 });
23 return { prepare: (sql: string) => statement(sql) } as unknown as D1Database;
24}
25
26const at = "2026-10-10T12:00:00Z";
27
28async function addAgent(db: D1Database, id: string, workspace: string, team: string | null, extra: { scope?: string; archived?: boolean } = {}): Promise<void> {
29 await db
30 .prepare(
31 `INSERT INTO agents (id, workspace_id, handle, display_name, role, instructions, routing, budget, autonomy, created_by, created_at, updated_at, team, scope, archived_at)
32 VALUES (?, ?, ?, ?, 'r', 'i', '{}', '{}', '{}', 'ana', ?, ?, ?, ?, ?)`,
33 )
34 .bind(id, workspace, id, id, at, at, team, extra.scope ?? "workspace", extra.archived ? at : null)
35 .run();
36}
37
38/** Identity's side: teams by workspace, and the memberships it holds. */
39function identity(teams: Record<string, string[]>) {
40 const on = new Set<string>();
41 const calls: string[] = [];
42 const adopt = async (workspaceId: string, agents: { agent_id: string; team: string }[]): Promise<AdoptedAgentTeam[]> => {
43 calls.push(workspaceId);
44 return agents.map(({ agent_id, team }) => {
45 const slug = team.trim().toLowerCase();
46 if (!(teams[workspaceId] ?? []).includes(slug)) return { agent_id, team, outcome: "no_team" };
47 const key = `${workspaceId}/${slug}/${agent_id}`;
48 if (on.has(key)) return { agent_id, team, outcome: "already" };
49 on.add(key);
50 return { agent_id, team, outcome: "added" };
51 });
52 };
53 return { adopt, on, calls };
54}
55
56test("only workspace agents that aren't archived join a team", () => {
57 const claims = claimsByWorkspace([
58 { id: "agt_a", workspace_id: "w1", team: "qa", scope: "workspace", archived_at: null },
59 { id: "agt_b", workspace_id: "w1", team: "qa", scope: "personal", archived_at: null },
60 { id: "agt_c", workspace_id: "w2", team: "sales", scope: null, archived_at: null },
61 { id: "agt_d", workspace_id: "w2", team: "sales", scope: "workspace", archived_at: at },
62 { id: "agt_e", workspace_id: "w2", team: " ", scope: "workspace", archived_at: null },
63 ]);
64 assert.deepEqual([...claims], [
65 ["w1", [{ agent_id: "agt_a", team: "qa" }]],
66 ["w2", [{ agent_id: "agt_c", team: "sales" }]],
67 ]);
68});
69
70test("agents' old teams become memberships once: matched by slug, the rest dropped, a second run does nothing", async () => {
71 const db = fakeD1();
72 await addAgent(db, "agt_margo", "w1", "QA");
73 await addAgent(db, "agt_david", "w1", "sales");
74 await addAgent(db, "agt_pax", "w1", "billing");
75 await addAgent(db, "agt_otto", "w2", "qa");
76 await addAgent(db, "agt_mine", "w1", "qa", { scope: "personal" });
77 await addAgent(db, "agt_none", "w1", null);
78 const fake = identity({ w1: ["qa", "sales"], w2: [] });
79 const first = await moveAgentTeams(db, fake.adopt, new Date(at));
80 assert.equal(first.added, 2);
81 assert.deepEqual(first.dropped.map((d) => d.agent_id).sort(), ["agt_otto", "agt_pax"]);
82 assert.equal(first.seen, 5, "the personal agent is marked without joining");
83 assert.deepEqual([...fake.on].sort(), ["w1/qa/agt_margo", "w1/sales/agt_david"]);
84 const second = await moveAgentTeams(db, fake.adopt, new Date(at));
85 assert.deepEqual(second, { seen: 0, added: 0, already: 0, dropped: [] });
86 assert.equal(fake.calls.length, 2, "identity is asked once per workspace, and never again");
87});
88
89test("a workspace identity couldn't answer for is asked again next run", async () => {
90 const db = fakeD1();
91 await addAgent(db, "agt_margo", "w1", "qa");
92 let fail = true;
93 const fake = identity({ w1: ["qa"] });
94 const adopt = async (id: string, agents: { agent_id: string; team: string }[]) => {
95 if (fail) throw new Error("identity is down");
96 return fake.adopt(id, agents);
97 };
98 assert.equal((await moveAgentTeams(db, adopt)).seen, 0);
99 fail = false;
100 assert.equal((await moveAgentTeams(db, adopt)).added, 1);
101});