Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-dashboard/test/agent-status.test.ts

Snapshot at 34e3175

Containing directory

11,930 bytes · typescriptView raw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3import { createElement } from "react";
4import { renderToStaticMarkup } from "react-dom/server";
5import { TooltipProvider } from "@cloudflare/kumo";
6import { createRequire } from "node:module";
7import { installDom } from "./dom.ts";
8import { mountDashboard } from "../src/dashboard.ts";
9import { createHttpTransport } from "../src/transport.ts";
10import { agentStatusRows } from "../src/agent-status-model.ts";
11import { AgentStatusTable, ResumeCard } from "../src/agent-status.tsx";
12import { stateView } from "../src/model.ts";
13import { AGENT_STATUS_NOW as now, agentStatusFixtureState } from "./agent-status-fixtures.ts";
14
15const fixture = (id = "agents-status") => stateView(agentStatusFixtureState(id, now));
16const rows = (id = "agents-status") => agentStatusRows(fixture(id), undefined, now);
17const brand = { name: "Fixture product", tagline: "Fixture collaboration" };
18const { JSDOM } = createRequire(import.meta.url)("jsdom") as {
19  JSDOM: new (html: string, options: { url: string }) => { window: Window & typeof globalThis };
20};
21const markup = (element: ReturnType<typeof createElement>) => renderToStaticMarkup(createElement(TooltipProvider, null, element));
22
23test("FH-078 first-match statuses use existing words and urgency", () => {
24  assert.deepEqual(rows().map((row) => row.status?.label ?? "Idle"), [
25    "Idle", "Blocked", "Returned", "Checks failed", "Timed out", "Couldn't run", "Checks running", "In review", "Approved", "In progress", "Racing", "Claimed, no recent push", "In progress", "Idle",
26  ]);
27  const state = fixture();
28  const blocked = state.claims[0];
29  state.claims.push({ ...blocked, packet: "fixture-second", agent: "fixture-agent", repoName: "fixture--fixture-agent" });
30  state.blockers[0].pushingPacket = "fixture-second";
31  state.blockers[0].repoName = "fixture--fixture-agent";
32  const row = agentStatusRows(state, undefined, now).find((entry) => entry.agent.name === "fixture-agent")!;
33  assert.equal(row.status?.label, "Blocked");
34  assert.equal(row.claims.length, 2);
35  assert.deepEqual(agentStatusRows(state, undefined, now).map((entry) => entry.identity?.number), rows().map((entry) => entry.identity?.number));
36});
37
38test("FH-078 held claims need 30 minutes of loaded activity before a quiet label", () => {
39  const held = rows().find((row) => row.agent.name === "held")!;
40  assert.equal(held.status?.label, "Claimed, no recent push");
41  assert.equal(held.status?.quietSince, now - 45 * 60_000);
42  const short = rows("agents-short-activity").find((row) => row.agent.name === "held")!;
43  assert.equal(short.status?.label, "In progress");
44  assert.equal(short.lastPush, undefined);
45  assert.equal(short.missingPush, "Not in loaded activity");
46  assert.equal(rows().find((row) => row.agent.name === "new")?.missingPush, "No push yet");
47});
48
49test("FH-078 a fresh claim without a push stays In progress through the 30-minute boundary", () => {
50  const state = fixture();
51  const fresh = (at: number) => agentStatusRows(state, undefined, at).find((row) => row.agent.name === "new")!;
52  assert.equal(fresh(now).status?.label, "In progress");
53  assert.equal(fresh(now).lastPush, undefined);
54  assert.equal(fresh(now).missingPush, "No push yet");
55  assert.equal(fresh(now + 29 * 60_000).status?.label, "In progress");
56  assert.equal(fresh(now + 29 * 60_000 + 1).status?.label, "Claimed, no recent push");
57  assert.equal(rows().find((row) => row.agent.name === "held")?.status?.quietSince, now - 45 * 60_000);
58  assert.equal(rows().find((row) => row.agent.name === "returned")?.status?.tone, "danger");
59});
60
61test("FH-078 a collision blocks the pushing fork, not the claimant's packet", () => {
62  const state = fixture();
63  state.blockers[0].packet = "fixture-agent";
64  const derived = agentStatusRows(state, undefined, now);
65  assert.equal(derived.find((row) => row.agent.name === "blocked")?.status?.label, "Blocked");
66  assert.equal(derived.find((row) => row.agent.name === "fixture-agent")?.status?.label, "In progress");
67  state.blockers[0].pushingPacket = null;
68  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "blocked")?.status?.label, "Blocked");
69});
70
71test("FH-078 old reviews and unrelated pushes do not impersonate current work", () => {
72  const state = fixture();
73  const review = state.reviews.find((entry) => entry.agent === "returned")!;
74  const push = state.recentActivity!.find((entry) => entry.agent === "returned")!;
75  push.recordedAt = new Date(now - 60_000).toISOString();
76  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "returned")?.status?.label, "In progress");
77  push.repoName = "unrelated-fork";
78  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "returned")?.status?.label, "Returned");
79  review.requestedAt = new Date(now - 100 * 60_000).toISOString();
80  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "returned")?.status?.label, "Claimed, no recent push");
81});
82
83test("FH-078 missing receipt settings do not imply checks are optional; approvals must be current", () => {
84  const state = fixture();
85  state.reviews.find((entry) => entry.agent === "review")!.receipt = null;
86  delete state.project.settings;
87  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "review")?.status?.label, "In progress");
88  state.project.settings = { requireReceipt: false };
89  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "review")?.status?.label, "In review");
90  state.forks.find((entry) => entry.agent === "approved")!.head = "9".repeat(40);
91  assert.equal(agentStatusRows(state, undefined, now).find((row) => row.agent.name === "approved")?.status?.label, "In progress");
92});
93
94test("FH-078 table collapses idle identities, keeps legacy live claims and changes owner copy", () => {
95  const state = fixture();
96  const props = { state, activity: state.recentActivity!, brand, openPacket() {} };
97  const owner = markup(createElement(AgentStatusTable, { ...props, owner: true }));
98  assert.match(owner, /Idle agents \(2\)/);
99  assert.doesNotMatch(owner, /data-agent="legacy"/);
100  assert.match(owner, /Resolve the collision/);
101  const agent = markup(createElement(AgentStatusTable, { ...props, owner: false }));
102  assert.match(agent, /the owner/);
103  assert.doesNotMatch(agent, /Resolve the collision|>Promote</);
104  state.agents.find((entry) => entry.name === "held")!.createdAt = null;
105  assert.match(markup(createElement(AgentStatusTable, { ...props, owner: true })), /Legacy/);
106});
107
108test("FH-078 reduced Resume card is read-only, absent for unknown callers and has no Next row", () => {
109  const state = fixture();
110  const props = { state, activity: state.recentActivity! };
111  assert.equal(markup(createElement(ResumeCard, props)), "");
112  assert.equal(markup(createElement(ResumeCard, { ...props, agent: "unknown" })), "");
113  const card = markup(createElement(ResumeCard, { ...props, agent: "fixture-agent" }));
114  assert.match(card, /Resume where you left off/);
115  assert.match(card, /fixture--fixture-agent/);
116  assert.match(card, /<dt>Packet<\/dt>|<dt>Last push<\/dt>|<dt>Status<\/dt>/);
117  assert.doesNotMatch(card, /<button|<dt>Next<\/dt>|unavailable/);
118  const idle = fixture("agent-idle");
119  assert.match(markup(createElement(ResumeCard, { state: idle, activity: idle.recentActivity!, agent: "fixture-agent" })), /No live claim\./);
120  const incomplete = fixture("agents-short-activity");
121  assert.match(markup(createElement(ResumeCard, { state: incomplete, activity: incomplete.recentActivity!, agent: "held" })), /Not in loaded activity/);
122});
123
124test("RV-rt3-1 the no-push placeholder in the table carries the wrapping class", () => {
125  const incomplete = fixture("agents-short-activity");
126  const table = markup(createElement(AgentStatusTable, { state: incomplete, activity: incomplete.recentActivity!, brand, owner: true, openPacket() {} }));
127  assert.match(table, /<span class="agent-missing-push caption muted">(Not in loaded activity|No push yet)<\/span>/);
128});
129
130async function until(check: () => boolean) {
131  for (let attempt = 0; attempt < 200; attempt++) {
132    if (check()) return;
133    await new Promise((resolve) => setTimeout(resolve, 5));
134  }
135  assert.fail("Agent status DOM did not settle");
136}
137
138test("FH-078 idle disclosure expands by keyboard-capable button and closes", async () => {
139  const dom = new JSDOM('<div id="dashboard"></div>', { url: "https://fixture.idiot.tax/#agents" });
140  installDom(dom.window);
141  const root = dom.window.document.getElementById("dashboard")!;
142  const wire = agentStatusFixtureState();
143  const calls: string[] = [];
144  const transport = createHttpTransport({ origin: "https://fixture.idiot.tax", fetch: async (input) => {
145    const path = new URL(String(input)).pathname;
146    calls.push(path);
147    return Response.json(path.endsWith("/state") ? wire : { activity: [], nextAfter: 0 });
148  } });
149  const app = mountDashboard(root, { brand, transport, initialProject: wire.project.id });
150  try {
151    await until(() => !!root.querySelector('[aria-controls="idle-agents"]'));
152    assert.equal(root.querySelector("#lanes table")?.getAttribute("aria-labelledby"), "agents-heading");
153    const packet = root.querySelector<HTMLButtonElement>('[data-agent="couldnt-run"] .agent-packet-link')!;
154    assert.equal(packet.textContent, "fixture-couldnt-run");
155    assert.equal(packet.getAttribute("aria-label"), "View packet fixture-couldnt-run");
156    const lastPush = root.querySelector('[data-agent="couldnt-run"] .agent-last-push-stacked')!;
157    assert.equal(lastPush.querySelector("code")?.textContent, "5555555");
158    assert.match(lastPush.querySelector("time.caption.muted")?.textContent ?? "", /min ago/);
159    const disclosure = root.querySelector<HTMLButtonElement>('[aria-controls="idle-agents"]')!;
160    assert.equal(disclosure.getAttribute("aria-expanded"), "false");
161    assert.equal(root.querySelector('[data-agent="legacy"]'), null);
162    disclosure.click();
163    await until(() => !!root.querySelector('[data-agent="legacy"]'));
164    assert.equal(disclosure.getAttribute("aria-expanded"), "true");
165    assert.match(root.querySelector('[data-agent="legacy"]')!.textContent!, /Idle.*Legacy|Legacy.*Idle/);
166    disclosure.click();
167    await until(() => !root.querySelector('[data-agent="legacy"]'));
168    assert.ok(calls.every((path) => /\/(state|activity)$/.test(path)));
169  } finally {
170    app.dispose();
171    dom.window.close();
172  }
173});
174
175test("FH-078 Resume is first in the agent Overview and absent for owner or unknown caller", async () => {
176  for (const [owner, agent, expected] of [[false, "fixture-agent", true], [false, undefined, false], [true, "fixture-agent", false]] as const) {
177    const dom = new JSDOM('<div id="dashboard"></div>', { url: "https://fixture.idiot.tax/#overview" });
178    installDom(dom.window);
179    const root = dom.window.document.getElementById("dashboard")!;
180    const wire = agentStatusFixtureState();
181    const transport = createHttpTransport({ origin: "https://fixture.idiot.tax", fetch: async (input) =>
182      Response.json(String(input).endsWith("/state") ? wire : { activity: [], nextAfter: 0 }) });
183    const app = mountDashboard(root, { brand, transport, initialProject: wire.project.id, owner, agent });
184    try {
185      await until(() => !!root.querySelector("#view-overview"));
186      assert.equal(!!root.querySelector(".resume-card"), expected);
187      if (expected) assert.ok(root.querySelector("#view-overview")!.firstElementChild?.classList.contains("resume-card"));
188      assert.equal(root.querySelector(".resume-card button"), null);
189      assert.equal(root.querySelector(".page-description")!.textContent!.trim(),
190        `Agents work in their own forks. ${owner ? "You review and promote." : "The owner reviews and promotes."}`);
191    } finally {
192      app.dispose();
193      dom.window.close();
194    }
195  }
196});
197