Public: anyone can read this; the crew can change it.
A test pins the home's idle reason: all busy says nothing, then no slots, the ceiling, or no ready work, in that order #6960
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6872-a-test-pins-the-home-s-idle-reason-all-b → main 1 commit fork
1 file
changed +83
−0
test/home-truth.test.ts added +83
| @@ -0,0 +1,83 @@ | ||
| 1 | /** The owner's home idle reason and waiting figures (#6872). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createRepo, moveArchived } from "../src/core/repos.ts"; | |
| 8 | import { createUser, type User } from "../src/core/users.ts"; | |
| 9 | import { id } from "../src/kernel/id.ts"; | |
| 10 | import { homeTruth, idleReason } from "../src/system/home_truth.ts"; | |
| 11 | ||
| 12 | test("idleReason says nothing when every slot runs, even over the ceiling", () => { | |
| 13 | expect(idleReason({ running: 3, slots: 3 }, { todayUsd: 0, ceilingUsd: 10 })).toBeNull(); | |
| 14 | expect(idleReason({ running: 4, slots: 3 }, { todayUsd: 99, ceilingUsd: 10 })).toBeNull(); | |
| 15 | }); | |
| 16 | ||
| 17 | test("idleReason: no slots first, then the ceiling, then no ready work", () => { | |
| 18 | expect(idleReason({ running: 0, slots: 0 }, { todayUsd: 99, ceilingUsd: 10 })).toBe("no slots set"); | |
| 19 | expect(idleReason({ running: 1, slots: 3 }, { todayUsd: 10, ceilingUsd: 10 })).toBe("daily ceiling of $10 reached"); | |
| 20 | expect(idleReason({ running: 1, slots: 3 }, { todayUsd: 12, ceilingUsd: 10 })).toBe("daily ceiling of $10 reached"); | |
| 21 | expect(idleReason({ running: 1, slots: 3 }, { todayUsd: 9.99, ceilingUsd: 10 })).toBe("no ready work"); | |
| 22 | expect(idleReason({ running: 0, slots: 3 }, { todayUsd: 0, ceilingUsd: 10 })).toBe("no ready work"); | |
| 23 | }); | |
| 24 | ||
| 25 | test("idleReason never answers the ceiling when there is none", () => { | |
| 26 | expect(idleReason({ running: 0, slots: 3 }, { todayUsd: 1e6, ceilingUsd: null })).toBe("no ready work"); | |
| 27 | }); | |
| 28 | ||
| 29 | let dataDir: string; | |
| 30 | let app: App; | |
| 31 | let owner: User; | |
| 32 | let stranger: User; | |
| 33 | const now = new Date("2026-10-27T12:00:00Z"); | |
| 34 | ||
| 35 | beforeAll(async () => { | |
| 36 | dataDir = await mkdtemp(join(tmpdir(), "forge-home-truth-")); | |
| 37 | app = createApp({ dataDir, port: 0 }); | |
| 38 | const mk = async (name: string, first: boolean) => { | |
| 39 | const r = await createUser(app.db, { origin: "seed", trusted: true, username: name, email: `${name}@forge.local`, password: "a-good-passphrase", adminIfFirst: first }); | |
| 40 | if (!r.ok) throw new Error(r.error); | |
| 41 | return r.value; | |
| 42 | }; | |
| 43 | owner = await mk("keeper", true); | |
| 44 | stranger = await mk("stranger", false); | |
| 45 | const repos: Record<string, string> = {}; | |
| 46 | for (const [name, visibility] of [["open", "public"], ["secret", "private"], ["shelved", "public"]] as const) { | |
| 47 | const r = await createRepo(app.db, app.reposRoot, { ownerId: owner.id, name, visibility, description: name }); | |
| 48 | if (!r.ok) throw new Error(r.error); | |
| 49 | repos[name] = r.value.id; | |
| 50 | } | |
| 51 | // Open issues assigned to the stranger: an assignment is an ask of its own. | |
| 52 | const assign = (repo: string, title: string, at: string) => { | |
| 53 | const number = app.db.query<{ issue_seq: number }, [string]>("UPDATE repo SET issue_seq = issue_seq + 1 WHERE id = ?1 RETURNING issue_seq").get(repos[repo]!)!.issue_seq; | |
| 54 | app.db | |
| 55 | .query(`INSERT INTO issue (id, repo_id, number, kind, title, body, author_id, state, created_at, updated_at, assignee_id) VALUES (?1, ?2, ?3, 'issue', ?4, '', ?5, 'open', ?6, ?6, ?7)`) | |
| 56 | .run(id(), repos[repo]!, number, title, owner.id, at, stranger.id); | |
| 57 | }; | |
| 58 | assign("open", "Newer", "2026-10-26T10:00:00.000Z"); | |
| 59 | assign("open", "Older", "2026-10-25T08:00:00.000Z"); | |
| 60 | assign("secret", "Hidden", "2026-10-01T00:00:00.000Z"); | |
| 61 | assign("shelved", "Put away", "2026-09-01T00:00:00.000Z"); | |
| 62 | moveArchived(app.db, repos.shelved!, true); | |
| 63 | }); | |
| 64 | ||
| 65 | afterAll(async () => { | |
| 66 | app.close(); | |
| 67 | await rm(dataDir, { recursive: true, force: true }); | |
| 68 | }); | |
| 69 | ||
| 70 | test("homeTruth counts only visible, unarchived asks and names the oldest time", () => { | |
| 71 | const w = homeTruth(app, stranger, now).waiting; | |
| 72 | expect(w.count).toBe(2); | |
| 73 | expect(w.oldestAt).toBe("2026-10-25T08:00:00.000Z"); | |
| 74 | }); | |
| 75 | ||
| 76 | test("homeTruth with nothing waiting has no oldest time", () => { | |
| 77 | expect(homeTruth(app, owner, now).waiting).toEqual({ count: 0, oldestAt: null }); | |
| 78 | }); | |
| 79 | ||
| 80 | test("homeTruth carries the idle reason beside the builders", () => { | |
| 81 | const t = homeTruth(app, owner, now); | |
| 82 | expect(t.builders.idle).toBe(idleReason(t.builders, t.spend)); | |
| 83 | }); |
new above
offline