Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/needs-you.test.ts
Snapshot at 34e3175
1import { installDom } from "./dom.ts";
2import { createRequire } from "node:module";
3import assert from "node:assert/strict";
4import { test } from "node:test";
5import { mountDashboard } from "../src/dashboard.ts";
6import { reviewState, stateView } from "../src/model.ts";
7import { ownerNeeds, copyText } from "../src/needs-you-model.ts";
8import { createHttpTransport } from "../src/transport.ts";
9import { needsFixtureState } from "./needs-you-fixtures.ts";
10import { fixtureReceipt } from "./fixtures.ts";
11
12const brand = { name: "Fixture product", tagline: "Offline" };
13const { JSDOM } = createRequire(import.meta.url)("jsdom");
14const now = Date.parse("2026-10-13T12:10:00Z");
15const kinds = ["promotionPending", "reconciler", "blocker", "failed", "promote", "approve"];
16const needs = (id = "needs-all") => ownerNeeds(stateView(needsFixtureState(id)), brand, now);
17
18test("six public state kinds retain evidence and exact priority without duplicating pending promotion", () => {
19 const wire = needsFixtureState("needs-all");
20 const state = stateView(wire);
21 assert.deepEqual(state.promotionPending, wire.promotionPending);
22 assert.equal(state.reconcilerAlerts.length, 1);
23 assert.equal(state.reconcilerAlerts[0].ref, "refs/heads/main");
24 const items = ownerNeeds(state, brand, now);
25 assert.deepEqual(
26 items.map((item) => item.kind),
27 kinds,
28 );
29 assert.match(copyText(items[0].why), /Fixture product started moving main aaaaaaa → bbbbbbb/);
30 assert.match(
31 copyText(items[2].why),
32 /fixture-reviewer pushed bbbbbbb to src\/claims.ts \+1, which fixture-builder has claimed/,
33 );
34 assert.equal(copyText(items[3].why), "1 of 3 checks failed · brand (13 s)");
35 assert.equal(copyText(items[4].outcome), "main moves aaaaaaa → ddddddd. Checks run again first.");
36 assert.equal(copyText(items[5].why), "Requested 10 min ago · Checks passed");
37 assert.equal(needs("needs-empty").length, 0);
38});
39
40test("oldest first uses durable receive sequence and review request time, independent of lexical wire order", () => {
41 const state = stateView(needsFixtureState("needs-all"));
42 const alert = state.reconcilerAlerts[0];
43 state.reconcilerAlerts = [
44 { ...alert, id: "10" },
45 { ...alert, id: "2" },
46 ];
47 const blocker = state.blockers[0];
48 state.blockers = [
49 { ...blocker, id: "10-p" },
50 { ...blocker, id: "2-p" },
51 ];
52 const review = state.reviews.at(-1)!;
53 state.reviews.push({ ...review, id: "earlier", requestedAt: "2026-10-12T12:00:00Z" });
54 const items = ownerNeeds(state, brand, now);
55 assert.deepEqual(
56 items.filter((i) => i.kind === "reconciler").map((i) => i.id),
57 ["alert:2", "alert:10"],
58 );
59 assert.deepEqual(
60 items.filter((i) => i.kind === "blocker").map((i) => i.id),
61 ["blocker:2-p", "blocker:10-p"],
62 );
63 assert.deepEqual(
64 items.filter((i) => i.kind === "approve").map((i) => i.id),
65 ["approve:earlier", `approve:${review.id}`],
66 );
67});
68
69test("stale, moved, returned, promoted and blocked reviews never claim readiness", () => {
70 for (const mode of [
71 "stale",
72 "moved",
73 "returned",
74 "promoted",
75 "blocked",
76 "missing-settings",
77 ] as const) {
78 const state = stateView(needsFixtureState("needs-approve"));
79 const review = state.reviews[0];
80 if (mode === "stale") review.status = "stale";
81 if (mode === "moved") state.forks[0].head = "9".repeat(40);
82 if (mode === "returned") review.returned = { reason: "Fix it", at: "2026-10-13T12:00:00Z" };
83 if (mode === "promoted") review.status = "promoted";
84 if (mode === "blocked")
85 state.blockers.push({
86 id: "1-p",
87 packet: "claimant-packet",
88 pushingPacket: review.packet,
89 repoName: review.repoName,
90 agent: "other",
91 paths: [],
92 reason: "Collision",
93 resolved: false,
94 });
95 if (mode === "missing-settings") delete state.project.settings;
96 assert.ok(
97 !ownerNeeds(state, brand).some((i) => i.kind === "approve" || i.kind === "promote"),
98 mode,
99 );
100 review.receipt = fixtureReceipt("failed");
101 if (["stale", "moved", "returned", "promoted"].includes(mode))
102 assert.equal(ownerNeeds(state, brand).length, 0, mode);
103 }
104});
105
106test("required interrupted checks get one evidence row, advisory failures stay ready to review", () => {
107 for (const status of ["failed", "timed-out", "infrastructure-error"] as const) {
108 const state = stateView(needsFixtureState("needs-promote"));
109 state.reviews[0].receipt = fixtureReceipt(status, "before-promote");
110 const items = ownerNeeds(state, brand);
111 assert.deepEqual(
112 items.map((i) => i.kind),
113 ["failed"],
114 );
115 assert.match(
116 copyText(items[0].outcome),
117 status === "failed" ? /pushes a fix/ : /Promote or a new push/,
118 );
119 }
120 assert.deepEqual(
121 needs("needs-advisory").map((i) => i.kind),
122 ["approve"],
123 );
124 assert.match(copyText(needs("needs-advisory")[0].why), /Checks advisory/);
125 const state = stateView(needsFixtureState("needs-failed"));
126 state.reviews[0].receipt!.checks = [state.reviews[0].receipt!.checks[0]];
127 assert.match(copyText(ownerNeeds(state, brand)[0].why), /^1 of 1 check failed/);
128});
129
130test("FH-045 derived Main moved excludes the nonstale approved loser and keeps the ready review", () => {
131 const wire = needsFixtureState("needs-main-moved");
132 assert.equal(wire.reviews[0].stale, false);
133 assert.ok(wire.reviews[0].approval);
134 const state = stateView(wire);
135 assert.equal(reviewState(state.reviews[0], state).kind, "main-moved");
136 assert.equal(reviewState(state.reviews[1], state).kind, "current");
137 const items = ownerNeeds(state, brand, now);
138 assert.deepEqual(
139 items.map((item) => copyText(item.title)),
140 ["Promote fixture-ready to main"],
141 );
142 state.reviews[0].receipt = fixtureReceipt("failed");
143 assert.deepEqual(
144 ownerNeeds(state, brand, now).map((item) => item.id),
145 [items[0].id],
146 );
147 state.reviews[1].stale = true;
148 assert.equal(reviewState(state.reviews[1], state).kind, "out-of-date");
149 assert.equal(ownerNeeds(state, brand, now).length, 0);
150});
151
152async function until(check: () => boolean) {
153 for (let i = 0; i < 200; i++) {
154 if (check()) return;
155 await new Promise((resolve) => setTimeout(resolve, 5));
156 }
157 assert.fail("Needs you DOM did not settle");
158}
159function setup(id = "needs-all", owner = true, detail = true) {
160 const dom = new JSDOM('<div id="dashboard"></div>', {
161 url: "https://fixture.idiot.tax/#overview",
162 });
163 installDom(dom.window);
164 const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
165 const wire = needsFixtureState(id);
166 const calls: { method: string; path: string }[] = [];
167 const transport = createHttpTransport({
168 origin: "https://fixture.idiot.tax",
169 fetch: async (input, init) => {
170 const path = new URL(String(input)).pathname;
171 calls.push({ method: init?.method ?? "GET", path });
172 if (path.endsWith("/state")) return Response.json(wire);
173 if (path.endsWith("/activity")) return Response.json({ activity: [], nextAfter: 0 });
174 return Response.json(
175 { error: "Synthetic detail intentionally unavailable" },
176 { status: 404 },
177 );
178 },
179 });
180 if (!detail) delete transport.getReview;
181 let invalidate: (() => void) | undefined;
182 transport.subscribe = (_project, listener) => {
183 invalidate = listener;
184 return () => {};
185 };
186 const app = mountDashboard(root, { brand, fixture: true, owner, initialProject: id, transport });
187 return {
188 dom,
189 root,
190 wire,
191 calls,
192 app,
193 invalidate: () => invalidate?.(),
194 close() {
195 app.dispose();
196 dom.window.close();
197 },
198 };
199}
200
201test("owner section is above project, semantic, limited to five, expandable and contains only evidence controls", async () => {
202 const c = setup();
203 try {
204 await until(() => !!c.root.querySelector(".needs-you-card"));
205 const card = c.root.querySelector(".needs-you-card")!;
206 assert.equal(card.parentElement!.getAttribute("aria-labelledby"), "needs-you-title");
207 assert.equal(card.parentElement!.nextElementSibling!.classList.contains("project-card"), true);
208 assert.equal(card.querySelectorAll("ul > li").length, 5);
209 assert.equal(
210 card.querySelectorAll('[data-action="approve"], [data-action="promote"]').length,
211 0,
212 );
213 const expand = [...card.querySelectorAll("button")].find(
214 (b) => b.textContent === "Show all 6",
215 )!;
216 expand.click();
217 await until(() => card.querySelectorAll("ul > li").length === 6);
218 for (const row of card.querySelectorAll(".needs-you-row")) {
219 assert.match(row.getAttribute("aria-label")!, /\./);
220 assert.equal(row.querySelectorAll('svg[aria-hidden="true"]').length, 2);
221 }
222 (card.querySelector('[data-need="approve"] button') as HTMLButtonElement).click();
223 await until(() => c.dom.window.location.hash.includes("#review/"));
224 assert.ok(c.calls.every((call) => call.method === "GET"));
225 } finally {
226 c.close();
227 }
228});
229
230test("queue-only host routes card to review queue without a dead detail hash", async () => {
231 const c = setup("needs-approve", true, false);
232 try {
233 await until(() => !!c.root.querySelector(".needs-you-row"));
234 (c.root.querySelector(".needs-you-row") as HTMLButtonElement).click();
235 assert.equal(c.dom.window.location.hash, "#review");
236 assert.equal(c.root.querySelector("#view-review")!.hasAttribute("hidden"), false);
237 assert.ok(c.calls.every((call) => call.method === "GET"));
238 } finally {
239 c.close();
240 }
241});
242
243test("collision opens Work details and reconciler routes to the matching agent row with focus", async () => {
244 const c = setup();
245 try {
246 await until(() => !!c.root.querySelector(".needs-you-row"));
247 (c.root.querySelector('[data-need="blocker"] button') as HTMLButtonElement).click();
248 assert.equal(c.dom.window.location.hash, "#work");
249 await until(
250 () => !!c.root.querySelector("#activity-panel")?.textContent?.includes("Collision evidence"),
251 );
252 (c.root.querySelector('[data-need="reconciler"] button') as HTMLButtonElement).click();
253 await until(
254 () =>
255 c.dom.window.document.activeElement?.getAttribute("data-agent-repo") ===
256 "fixture-project--external",
257 );
258 assert.equal(c.dom.window.location.hash, "#agents");
259 } finally {
260 c.close();
261 }
262});
263
264test("live invalidation removes resolved work to persistent empty card; agents have no card or shell", async () => {
265 const c = setup("needs-approve");
266 try {
267 await until(() => !!c.root.querySelector(".needs-you-row"));
268 c.wire.reviews[0].returned = { reason: "Fixture fix", at: "2026-10-13T12:00:00Z" };
269 c.invalidate();
270 await until(() => !!c.root.querySelector(".needs-you-empty"));
271 assert.equal(
272 c.root.querySelector(".needs-you-empty")!.textContent,
273 "Nothing needs you right now.",
274 );
275 assert.equal(c.root.querySelectorAll(".needs-you-row").length, 0);
276 } finally {
277 c.close();
278 }
279 const agent = setup("needs-all", false);
280 try {
281 await until(() => !!agent.root.querySelector(".project-card"));
282 assert.equal(agent.root.querySelector('[aria-labelledby="needs-you-title"]'), null);
283 } finally {
284 agent.close();
285 }
286});
287