Public: anyone can read this; the crew can change it.

A test proves the crew governor: a repository fills the smaller of the spend pacing's and the governor's slots, and one `crew.governed` event is said per change #7529

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/7523-a-test-proves-the-crew-governor-a-reposi → main 2 commits fork

  • Merged. scout/7523-a-test-proves-the-crew-governor-a-reposi was merged into main as fbdae31.

Review

Sign in to review this change.
1 file changed +160 −0
test/crew-governed-unit.test.ts added +160
@@ -0,0 +1,160 @@
1 /**
2 * `governorFor` and `announceGovernor` (src/crew/governed.ts) on their own:
3 * the governor's slots read from the real vitals, one `crew.governed` event
4 * per change of the governed number, the reason returned only while the
5 * governor holds the crew below the operator's slots, and `repoPace` taking
6 * the smaller of spend pacing's slots and the governor's. The picker itself
7 * (runs started, the feed, the tick line) is proved in crew-governed.test.ts.
8 * Each test has its own database, so none depends on another's pulls.
9 */
10 import { afterEach, expect, test } from "bun:test";
11 import { mkdtempSync, rmSync } from "node:fs";
12 import { tmpdir } from "node:os";
13 import { join } from "node:path";
14 import { createApp, type App } from "../src/app.ts";
15 import { createRepo, type Repo } from "../src/core/repos.ts";
16 import { createRun } from "../src/core/runs.ts";
17 import { noteLimit, setHeadroom } from "../src/core/standing.ts";
18 import { addSubscription, setGrants } from "../src/core/subscriptions.ts";
19 import { recordUsage } from "../src/core/usage.ts";
20 import { createUser } from "../src/core/users.ts";
21 import { repoPace } from "../src/crew/autonomy.ts";
22 import { announceGovernor, governorFor } from "../src/crew/governed.ts";
23 import { governedSlots } from "../src/crew/governor.ts";
24 import { readVitals } from "../src/flow/vitals.ts";
25 import { id } from "../src/kernel/id.ts";
26
27 const NOW = new Date("2026-10-02T15:00:00Z");
28 const ago = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
29
30 const cleanup: (() => void)[] = [];
31 afterEach(() => {
32 while (cleanup.length > 0) cleanup.pop()!();
33 });
34
35 /** A fresh forge with one repository whose operator's slots are `slots`. */
36 async function forge(slots: number) {
37 const root = mkdtempSync(join(tmpdir(), "forge-governed-unit-"));
38 const app: App = createApp({ dataDir: root, port: 0 });
39 cleanup.push(() => {
40 app.close();
41 rmSync(root, { recursive: true, force: true });
42 });
43 const u = await createUser(app.db, { username: "alice", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true, adminIfFirst: true });
44 if (!u.ok) throw new Error(u.error);
45 const r = await createRepo(app.db, app.reposRoot, { ownerId: u.value.id, name: "gov", visibility: "public", description: "g" });
46 if (!r.ok) throw new Error(r.error);
47 const repo: Repo = { ...r.value, autonomySlots: slots };
48 let n = 0;
49 /** A pull opened `openedH` hours before NOW; merged `mergedH` hours before it, when given. */
50 const pull = (openedH: number, mergedH: number | null): void => {
51 const issueId = id();
52 n++;
53 app.db
54 .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at) VALUES (?1, ?2, ?3, 'pull', ?4, '', ?5, ?6, ?7)`)
55 .run(issueId, repo.id, n, `P${n}`, u.value.id, mergedH === null ? "open" : "closed", ago(openedH));
56 app.db
57 .query(`INSERT INTO pull (issue_id, base_branch, head_branch, base_oid, head_oid, head_repo_id, merged_at) VALUES (?1, ?2, ?3, '', '', ?4, ?5)`)
58 .run(issueId, repo.defaultBranch, `b/${n}`, repo.id, mergedH === null ? null : ago(mergedH));
59 };
60 const events = () =>
61 app.db
62 .query<{ payload: string }, [string]>(`SELECT payload FROM event WHERE kind = 'crew.governed' AND repo_id = ?1 ORDER BY rowid`)
63 .all(repo.id)
64 .map((e) => JSON.parse(e.payload) as { from: number; to: number; why: string });
65 return { app, repo, pull, events, userId: u.value.id };
66 }
67
68 const later = (s: number) => new Date(NOW.getTime() + s * 1000);
69
70 test("a quiet forge answers the operator's slots, says nothing, and the pace follows", async () => {
71 const { app, repo, events } = await forge(5);
72 const g = governorFor(app, repo, NOW);
73 expect(g.slots).toBe(5);
74 expect(g.why).toContain("Builders 5");
75 expect(announceGovernor(app, repo, NOW)).toBeUndefined();
76 expect(events()).toEqual([]);
77 expect(repoPace(app, repo, NOW).slots).toBe(5);
78 });
79
80 test("a pile lowers the slots: governorFor and the pace take the smaller, announce says it once", async () => {
81 const { app, repo, pull, events } = await forge(4);
82 for (let i = 0; i < 12; i++) pull(20 + i, null); // 12 open, none landed
83 const vitals = readVitals(app.db, NOW);
84 expect(vitals).toMatchObject({ open: 12, landed1h: 0 });
85 const want = governedSlots(vitals, 4);
86 expect(want.slots).toBeLessThan(4);
87
88 const g = governorFor(app, repo, NOW);
89 expect(g).toEqual(want);
90 expect(g.why).toContain("12 open vs 0 landed");
91 // No subscription accounts: spend pacing allows all 4, so the governor's is the smaller.
92 expect(repoPace(app, repo, NOW).slots).toBe(g.slots);
93
94 expect(announceGovernor(app, repo, NOW)).toBe(g.why);
95 expect(announceGovernor(app, repo, later(1))).toBe(g.why);
96 expect(announceGovernor(app, repo, later(2))).toBe(g.why);
97 expect(events()).toEqual([{ from: 4, to: g.slots, why: g.why }]);
98 });
99
100 test("spend pacing is the smaller when the windows are nearly spent, the governor when the pile is", async () => {
101 const { app, repo, pull, userId } = await forge(5);
102 const added = addSubscription(app.db, app.config.dataDir, { name: "acct", token: "sk-ant-oat01-governed-test" });
103 if (!added.ok) throw new Error(added.error);
104 const sub = added.value.id;
105 setGrants(app.db, repo.id, userId, [sub]);
106 expect(setHeadroom(app.db, sub, 0)).toBe(true);
107 const spend = (start: string, usd: number): void => {
108 recordUsage(app.db, {
109 runId: createRun(app.db, { repoId: repo.id, kind: "agent" }).id,
110 repoId: repo.id, issueId: null, agentId: null, subscriptionId: sub,
111 tokensIn: 1000, tokensOut: 100, costUsd: usd, wallMs: 60_000, outcome: "ok",
112 at: new Date(Date.parse(start) + 60_000).toISOString(),
113 });
114 };
115 // The provider refused at a $5 cap in the window 09:00–14:00; a run for $3 in the next window leaves $2.
116 spend("2026-10-02T09:30:00Z", 5);
117 noteLimit(app.db, sub, { window: "5h", resetsAt: "2026-10-02T14:00:00.000Z", observedAt: "2026-10-02T11:00:00.000Z", said: "You've hit your session limit · resets 2pm (UTC)" });
118 spend("2026-10-02T14:30:00Z", 3);
119
120 // Quiet forge: the governor allows 5, spend pacing allows 2; the pace is spend's.
121 expect(governorFor(app, repo, NOW).slots).toBe(5);
122 const paced = repoPace(app, repo, NOW);
123 expect(paced.slots).toBe(2);
124 expect(paced.reason).toContain("windows are filling");
125 // The reason the tick line carries is the governor's, and it holds nothing back here.
126 expect(announceGovernor(app, repo, NOW)).toBeUndefined();
127
128 // A pile: the governor's 1 is now the smaller of the two.
129 for (let i = 0; i < 12; i++) pull(20 + i, null);
130 expect(governorFor(app, repo, NOW).slots).toBe(1);
131 expect(repoPace(app, repo, NOW).slots).toBe(1);
132 });
133
134 test("the reason is absent when the governed number equals the operator's slots", async () => {
135 const one = await forge(1);
136 for (let i = 0; i < 12; i++) one.pull(20 + i, null);
137 expect(governorFor(one.app, one.repo, NOW).slots).toBe(1);
138 expect(announceGovernor(one.app, one.repo, NOW)).toBeUndefined();
139 expect(one.events()).toEqual([]);
140 const off = await forge(0);
141 expect(announceGovernor(off.app, off.repo, NOW)).toBeUndefined();
142 expect(off.events()).toEqual([]);
143 });
144
145 test("a recovery is a second change: one more event, then no reason", async () => {
146 const { app, repo, pull, events } = await forge(4);
147 for (let i = 0; i < 12; i++) pull(20 + i, null);
148 expect(readVitals(app.db, NOW)).toMatchObject({ open: 12, landed1h: 0 });
149 expect(announceGovernor(app, repo, NOW)).toBeDefined();
150 const held = governorFor(app, repo, NOW).slots;
151 expect(held).toBeLessThan(4);
152
153 // Six more land in the last hour: 12 open is no more than 3 x 6 landed, and the hour's inflow is zero.
154 for (let i = 0; i < 6; i++) pull(30 + i, 0.1);
155 expect(readVitals(app.db, later(5))).toMatchObject({ open: 12, landed1h: 6, opened1h: 0 });
156 expect(governorFor(app, repo, later(5)).slots).toBe(4);
157 expect(announceGovernor(app, repo, later(5))).toBeUndefined();
158 expect(announceGovernor(app, repo, later(6))).toBeUndefined();
159 expect(events().map((e) => [e.from, e.to])).toEqual([[4, held], [held, 4]]);
160 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs