Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/collisions.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 { blockerView, collisionHistory, reviewGate, stateView } from "../src/model.ts";
7import { ownerNeeds, copyText } from "../src/needs-you-model.ts";
8import { packetStatus } from "../src/view.tsx";
9import { createHttpTransport } from "../src/transport.ts";
10import { collisionFixtureState } from "./collision-fixtures.ts";
11import { reviewFixtureDetail } from "./review-fixtures.ts";
12const { JSDOM } = createRequire(import.meta.url)("jsdom");
13const brand = { name: "Fixture product", tagline: "Offline fixture" };
14const origin = "https://fixture.idiot.tax";
15
16test("wire resolution, legacy fallback and pushing-fork gate share one contract", () => {
17 const wire = collisionFixtureState();
18 const open = wire.blockers[0];
19 for (const resolution of [
20 undefined,
21 null,
22 { at: "2026-10-13T14:02:00Z", by: "owner" as const, reason: "Settled" },
23 ]) {
24 open.resolved = resolution;
25 const state = stateView(wire);
26 const [pusher, claimant] = state.reviews;
27 assert.equal(state.blockers[0].resolved, resolution != null);
28 assert.equal(reviewGate(pusher, state).approve, resolution != null);
29 assert.equal(reviewGate(claimant, state).approve, true);
30 assert.equal(packetStatus(claimant.packet, state).label, "In review");
31 assert.equal(packetStatus(pusher.packet, state).label, resolution ? "In review" : "Blocked");
32 assert.equal(
33 ownerNeeds(state, brand).filter((i) => i.kind === "blocker").length,
34 resolution ? 0 : 1,
35 );
36 }
37 open.resolved = null;
38 const state = stateView(wire);
39 const review = state.reviews[0];
40 assert.equal(reviewGate({ ...review, packet: "another-packet" }, state).approve, true);
41 state.blockers[0].pushingPacket = null;
42 assert.equal(reviewGate({ ...review, packet: "another-packet" }, state).approve, false);
43 assert.match(reviewGate(review, state, false).reason, /Waiting for the owner/);
44 assert.doesNotMatch(reviewGate(review, state, false).reason, /Mark the collision/);
45 const need = ownerNeeds(state, brand).find((n) => n.kind === "blocker")!;
46 assert.match(copyText(need.outcome), /reviews from/);
47 assert.deepEqual(need.target, { packet: "claimant-packet", collision: "collision-1" });
48});
49
50test("history sorts open by real push times, legacy stably, then resolved by resolution time", () => {
51 const state = stateView(collisionFixtureState("collision-history"));
52 const open = state.blockers[0];
53 const blockers = [
54 ...state.blockers,
55 { ...open, id: "legacy-1", at: null },
56 { ...open, id: "newer", at: "2026-10-13T14:02:00Z" },
57 { ...open, id: "legacy-2", at: null },
58 ];
59 assert.deepEqual(
60 collisionHistory(blockers).map((b) => b.id),
61 [
62 "newer",
63 "collision-1",
64 "legacy-1",
65 "legacy-2",
66 "resolved-owner",
67 "resolved-release",
68 "resolved-promote",
69 ],
70 );
71 assert.equal(blockerView({ ...collisionFixtureState().blockers[0], at: undefined }).at, null);
72});
73
74async function until(check: () => unknown) {
75 for (let i = 0; i < 250; i++) {
76 if (check()) return;
77 await new Promise((r) => setTimeout(r, 5));
78 }
79 assert.fail("Collision DOM did not settle");
80}
81function setup({
82 owner = true,
83 scenario = "collision-open",
84 route = "work",
85 outcome = "owner",
86 detailMode = "complete",
87}: {
88 detailMode?: "complete" | "omitted" | "stale";
89 owner?: boolean;
90 scenario?: string;
91 route?: string;
92 outcome?: "owner" | "release" | "promote" | "missing" | "error";
93} = {}) {
94 const dom = new JSDOM('<div id="dashboard"></div>', { url: `${origin}/#${route}` });
95 installDom(dom.window);
96 const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
97 const wire = collisionFixtureState(scenario);
98 const calls: { path: string; init?: RequestInit }[] = [];
99 const transport = createHttpTransport({
100 origin,
101 fetch: async (input, init) => {
102 const path = new URL(String(input)).pathname;
103 calls.push({ path, init });
104 if (path.endsWith("/state")) return Response.json(wire);
105 if (path.endsWith("/activity")) return Response.json({ activity: [], nextAfter: 0 });
106 if (path.endsWith("/resolve")) {
107 if (outcome === "missing") {
108 wire.blockers = [];
109 return Response.json({ error: "private raw error" }, { status: 404 });
110 }
111 if (outcome === "error")
112 return Response.json({ error: "private raw error" }, { status: 503 });
113 const blocker = wire.blockers[0];
114 blocker.resolved = {
115 at: "2026-10-13T14:05:00Z",
116 by: outcome,
117 ...(outcome === "owner" ? { reason: JSON.parse(String(init!.body)).reason } : {}),
118 };
119 return Response.json({ blocker });
120 }
121 const review = wire.reviews.find((r) => path.endsWith(`/${r.id}`))!;
122 return Response.json({
123 ...reviewFixtureDetail({ ...wire, reviews: [review] }),
124 blockers:
125 detailMode === "omitted"
126 ? []
127 : detailMode === "stale"
128 ? wire.blockers.map((b) => ({ ...b, resolved: null }))
129 : wire.blockers.filter((b) => b.packet === review.packet),
130 packet: wire.packets.find((p) => p.id === review.packet)!,
131 });
132 },
133 });
134 const app = mountDashboard(root, {
135 brand,
136 fixture: true,
137 owner,
138 initialProject: scenario,
139 transport,
140 });
141 return {
142 dom,
143 root,
144 wire,
145 calls,
146 app,
147 close() {
148 app.dispose();
149 dom.window.close();
150 },
151 };
152}
153function fill(c: ReturnType<typeof setup>, value: string) {
154 const textarea = c.dom.window.document.querySelector("textarea")!;
155 Object.getOwnPropertyDescriptor(c.dom.window.HTMLTextAreaElement.prototype, "value")!.set!.call(
156 textarea,
157 value,
158 );
159 textarea.dispatchEvent(new c.dom.window.Event("input", { bubbles: true }));
160}
161async function openDialog(c: ReturnType<typeof setup>) {
162 await until(() => c.root.querySelector('[aria-label^="Mark resolved:"]'));
163 const control = c.root.querySelector('[aria-label^="Mark resolved:"]') as HTMLButtonElement;
164 control.focus();
165 control.click();
166 await until(() => c.dom.window.document.querySelector("textarea"));
167}
168
169test("owner dialog enforces reason, focuses input, posts once and refreshes counts, gate and history", async () => {
170 const c = setup();
171 try {
172 await openDialog(c);
173 const doc = c.dom.window.document;
174 const primary = () => doc.querySelector('[data-action="confirm-resolve"]') as HTMLButtonElement;
175 assert.equal(primary().disabled, true);
176 await until(() => doc.activeElement?.textContent === "Coordinated: both changes can land.");
177 const quickReasons = [...doc.querySelectorAll('[aria-label="Quick reasons"] button')];
178 assert.equal(quickReasons.length, 3);
179 for (const button of quickReasons) {
180 assert.match(button.className, /ring-kumo-line/);
181 assert.match(button.className, /bg-kumo-base/);
182 }
183 fill(c, " \n ");
184 await until(() => primary().disabled);
185 fill(c, "x".repeat(200));
186 await until(() => !primary().disabled);
187 fill(c, "x".repeat(201));
188 await until(() => primary().disabled);
189 fill(c, "Agreed.\n<script>plain text</script>");
190 await until(() => !primary().disabled);
191 assert.match(doc.querySelector('[role="dialog"]')!.textContent!, /35 \/ 200/);
192 primary().click();
193 await until(() => !doc.querySelector(".collision-dialog"));
194 assert.equal(c.root.querySelector("#blockers"), null);
195 assert.match(doc.body.textContent!, /Marked resolved: pushing-packet is unblocked/);
196 assert.doesNotMatch(c.root.querySelector("#packets")!.textContent!, /Blocked/);
197 const writes = c.calls.filter((call) => call.init?.method === "POST");
198 assert.equal(writes.length, 1);
199 assert.equal(writes[0].path, "/projects/collision-open/blockers/collision-1/resolve");
200 assert.equal(writes[0].init?.credentials, "same-origin");
201 assert.equal(writes[0].init?.redirect, "error");
202 assert.equal(
203 JSON.parse(String(writes[0].init?.body)).reason,
204 "Agreed.\n<script>plain text</script>",
205 );
206 await c.app.refresh();
207 assert.equal(reviewGate(stateView(c.wire).reviews[0], stateView(c.wire)).approve, true);
208 assert.equal(
209 ownerNeeds(stateView(c.wire), brand).some((n) => n.kind === "blocker"),
210 false,
211 );
212 } finally {
213 c.close();
214 }
215});
216
217test("safe error dialog survives refresh; 404 hides primary and focuses Close", async () => {
218 for (const outcome of ["missing", "error"] as const) {
219 const c = setup({ outcome });
220 try {
221 await openDialog(c);
222 fill(c, "Settled");
223 await until(
224 () =>
225 !(
226 c.dom.window.document.querySelector(
227 '[data-action="confirm-resolve"]',
228 ) as HTMLButtonElement
229 ).disabled,
230 );
231 (
232 c.dom.window.document.querySelector('[data-action="confirm-resolve"]') as HTMLButtonElement
233 ).click();
234 await until(() => c.dom.window.document.querySelector('[role="dialog"] [role="alert"]'));
235 const dialog = c.dom.window.document.querySelector('[role="dialog"]')!;
236 assert.match(
237 dialog.textContent!,
238 outcome === "missing"
239 ? /This collision doesn't exist anymore/
240 : /Couldn't mark it resolved. Try again/,
241 );
242 assert.doesNotMatch(dialog.textContent!, /private raw error/);
243 assert.equal(
244 !!dialog.querySelector('[data-action="confirm-resolve"]'),
245 outcome !== "missing",
246 );
247 assert.equal(c.calls.filter((call) => call.init?.method === "POST").length, 1);
248 assert.ok(c.calls.filter((call) => call.path.endsWith("/state")).length >= 2);
249 if (outcome === "missing")
250 await until(() => c.dom.window.document.activeElement?.textContent === "Close");
251 } finally {
252 c.close();
253 }
254 }
255});
256
257test("idempotent race toast uses core's release/promote cause", async () => {
258 for (const outcome of ["release", "promote"] as const) {
259 const c = setup({ outcome });
260 try {
261 await openDialog(c);
262 fill(c, "Settled");
263 await until(
264 () =>
265 !(
266 c.dom.window.document.querySelector(
267 '[data-action="confirm-resolve"]',
268 ) as HTMLButtonElement
269 ).disabled,
270 );
271 (
272 c.dom.window.document.querySelector('[data-action="confirm-resolve"]') as HTMLButtonElement
273 ).click();
274 await until(() => c.dom.window.document.body.textContent.includes("was already unblocked"));
275 assert.match(
276 c.dom.window.document.body.textContent,
277 outcome === "release" ? /claim was released/ : /claim was promoted/,
278 );
279 } finally {
280 c.close();
281 }
282 }
283});
284
285test("agent has no resolve controls; claimant review has Approve and no banner; review and packet retain quiet history", async () => {
286 for (const owner of [true, false]) {
287 const c = setup({ owner, scenario: "collision-history", route: "review/review-1" });
288 try {
289 await until(() => c.root.querySelector(".review-detail .collision-row"));
290 assert.equal(c.root.querySelectorAll(".review-detail .collision-row").length, 3);
291 assert.equal(!!c.root.querySelector('[aria-label^="Mark resolved:"]'), owner);
292 assert.equal(c.root.querySelector("script"), null);
293 assert.match(c.root.textContent!, /<script>plain text<\/script>/);
294 assert.match(c.root.textContent!, owner ? /by you/ : /by the owner/);
295 const packetButton = [...c.root.querySelectorAll("button")].find(
296 (b) => b.textContent === "View packet",
297 )!;
298 packetButton.click();
299 await until(() => c.root.querySelector('[aria-label="Collisions"] .collision-row'));
300 assert.equal(c.root.querySelectorAll('[aria-label="Collisions"] .collision-row').length, 4);
301 } finally {
302 c.close();
303 }
304 }
305 const c = setup({ route: "review/claimant-review" });
306 try {
307 await until(() => c.root.querySelector('.review-detail [data-action="approve"]'));
308 assert.equal(c.root.querySelector(".review-detail .blockers"), null);
309 } finally {
310 c.close();
311 }
312});
313
314test("snapshot blockers survive omitted detail and newer resolved snapshot wins over stale open detail", async () => {
315 for (const detailMode of ["omitted", "stale"] as const) {
316 const c = setup({ route: "review/review-1", detailMode });
317 try {
318 if (detailMode === "stale") {
319 c.wire.blockers[0].resolved = { at: "2026-10-13T14:05:00Z", by: "release" };
320 await until(() => c.root.querySelector(".review-milestones"));
321 await c.app.refresh();
322 await until(() => c.root.querySelector('.review-detail [data-action="approve"]'));
323 assert.equal(
324 c.root.querySelector(".review-detail .blockers [aria-label^='Mark resolved:']"),
325 null,
326 );
327 assert.match(c.root.querySelector(".review-detail")!.textContent!, /Resolved/);
328 } else {
329 await until(() => c.root.querySelector('.review-detail [aria-label^="Mark resolved:"]'));
330 assert.equal(c.root.querySelector('.review-detail [data-action="approve"]'), null);
331 }
332 } finally {
333 c.close();
334 }
335 }
336});
337
338test("Needs you focuses the collision, quick reasons only fill, keyboard submits once and success offers review", async () => {
339 const c = setup({ route: "overview" });
340 try {
341 await until(() => c.root.querySelector('.needs-you-row[aria-label^="Sort out"]'));
342 (c.root.querySelector('.needs-you-row[aria-label^="Sort out"]') as HTMLButtonElement).click();
343 await until(() => c.dom.window.document.activeElement?.closest(".collision-highlight"));
344 const control = c.dom.window.document.activeElement as HTMLButtonElement;
345 assert.match(control.getAttribute("aria-label")!, /^Mark resolved/);
346 assert.equal(c.dom.window.location.hash, "#work");
347 control.click();
348 await until(() => c.dom.window.document.querySelector(".collision-dialog"));
349 const doc = c.dom.window.document;
350 await until(() => doc.activeElement?.textContent === "Coordinated: both changes can land.");
351 (doc.activeElement as HTMLButtonElement).click();
352 await until(() => doc.activeElement === doc.querySelector("textarea"));
353 assert.equal(
354 (doc.querySelector("textarea") as HTMLTextAreaElement).value,
355 "Coordinated: both changes can land.",
356 );
357 assert.equal(
358 c.calls.some((call) => call.init?.method === "POST"),
359 false,
360 );
361 fill(c, "Coordinated and checked.");
362 await until(() => doc.querySelector(".collision-dialog")?.textContent?.includes("24 / 200"));
363 doc
364 .querySelector("textarea")!
365 .dispatchEvent(
366 new c.dom.window.KeyboardEvent("keydown", { bubbles: true, key: "Enter", ctrlKey: true }),
367 );
368 await until(() => !doc.querySelector(".collision-dialog"));
369 await until(() => doc.activeElement?.classList.contains("collision-heading"));
370 assert.equal(c.calls.filter((call) => call.init?.method === "POST").length, 1);
371 assert.ok(c.root.querySelector('[aria-label="Collisions"] .claim-link'));
372 assert.ok([...doc.querySelectorAll("button")].some((b) => b.textContent === "Open review"));
373 } finally {
374 c.close();
375 }
376});
377
378test("Escape cancels with no write and dialog evidence shows five paths plus full-list tooltip", async () => {
379 const c = setup();
380 try {
381 await until(() => c.root.querySelector("#packets"));
382 c.wire.blockers[0].paths = Array.from({ length: 7 }, (_, i) => `src/overlap-${i}.ts`);
383 await c.app.refresh();
384 await openDialog(c);
385 const doc = c.dom.window.document;
386 assert.equal(doc.querySelectorAll(".collision-evidence code").length, 5);
387 assert.match(doc.querySelector(".collision-evidence")!.textContent!, /\+2 more/);
388 doc.activeElement!.dispatchEvent(
389 new c.dom.window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }),
390 );
391 await until(() => !doc.querySelector(".collision-dialog"));
392 assert.equal(
393 c.calls.some((call) => call.init?.method === "POST"),
394 false,
395 );
396 await until(() => doc.activeElement?.getAttribute("aria-label")?.startsWith("Mark resolved"));
397 } finally {
398 c.close();
399 }
400});
401
402test("packet history offers Open review only once on the newest resolved row and removes it when the review ceases to be current", async () => {
403 const c = setup({ scenario: "collision-history", route: "work" });
404 try {
405 await until(() => c.root.querySelector("#blockers"));
406 (
407 [...c.root.querySelectorAll("button")].find(
408 (button) => button.textContent === "View packet",
409 ) as HTMLButtonElement
410 ).click();
411 await until(() => c.root.querySelector('[aria-label="Collisions"] .claim-link'));
412 const panel = () => c.root.querySelector('[aria-label="Collisions"]')!;
413 const links = () =>
414 [...panel().querySelectorAll("button")].filter(
415 (button) => button.textContent === "Open review",
416 );
417 assert.equal(panel().querySelectorAll(".collision-row").length, 4);
418 assert.equal(links().length, 1);
419 assert.equal(
420 links()[0].closest("[data-collision]")?.getAttribute("data-collision"),
421 "resolved-owner",
422 );
423 // Date order, not input order or the currently highlighted open row, selects the link.
424 c.wire.blockers.find((blocker) => blocker.id === "resolved-release")!.resolved!.at =
425 "2026-10-13T15:00:00Z";
426 await c.app.refresh();
427 assert.equal(links().length, 1);
428 assert.equal(
429 links()[0].closest("[data-collision]")?.getAttribute("data-collision"),
430 "resolved-release",
431 );
432 const review = c.wire.reviews[0];
433 review.returned = { at: "2026-10-13T15:01:00Z", reason: "Fix before returning" };
434 await c.app.refresh();
435 assert.equal(links().length, 0);
436 review.returned = null;
437 c.wire.forks.find((fork) => fork.repoName === review.repoName)!.head = "9".repeat(40);
438 await c.app.refresh();
439 assert.equal(links().length, 0);
440 c.wire.forks = c.wire.forks.filter((fork) => fork.repoName !== review.repoName);
441 await c.app.refresh();
442 assert.equal(links().length, 0);
443 c.wire.reviews = c.wire.reviews.filter((entry) => entry.id !== review.id);
444 await c.app.refresh();
445 assert.equal(links().length, 0);
446 } finally {
447 c.close();
448 }
449});
450