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

packages/forkhorse-dashboard/test/collisions.test.ts

Snapshot at 34e3175

Containing directory

18,386 bytes · typescriptView raw
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