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

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

Snapshot at 34e3175

Containing directory

27,259 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 { createHttpTransport } from "../src/transport.ts";
7import {
8  promotionReceipt,
9  recentReceipts,
10  receiptGateCaption,
11  reviewGate,
12  stateView,
13} from "../src/model.ts";
14import { relativeTime } from "../src/time.ts";
15import { duration, ReceiptDetails } from "../src/receipts.tsx";
16import { fixtureOwnerReceipt, fixtureReceipt, fixtureState } from "./fixtures.ts";
17import { createElement } from "react";
18import { createRoot } from "react-dom/client";
19import { flushSync } from "react-dom";
20
21const { JSDOM } = createRequire(import.meta.url)("jsdom");
22const origin = "https://fixture.idiot.tax";
23async function until(check: () => boolean) {
24  for (let i = 0; i < 200; i++) {
25    if (check()) return;
26    await new Promise((resolve) => setTimeout(resolve, 5));
27  }
28  assert.fail("Receipt DOM did not settle");
29}
30function setup(id = "receipts", owner = true, fetcher?: typeof fetch) {
31  const dom = new JSDOM('<div id="dashboard"></div>', { url: `${origin}/#review` });
32  installDom(dom.window);
33  const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
34  const wire = fixtureState(id);
35  const calls: { path: string; method: string; body?: BodyInit | null }[] = [];
36  const transport = createHttpTransport({
37    origin,
38    fetch:
39      fetcher ??
40      (async (input, init) => {
41        const path = new URL(String(input)).pathname;
42        calls.push({ path, method: init?.method ?? "GET", body: init?.body });
43        if (path.endsWith("/state")) return Response.json(wire);
44        if (path.endsWith("/activity")) return Response.json({ activity: [], nextAfter: 0 });
45        if (path.endsWith("/settings")) {
46          wire.project.settings = JSON.parse(String(init!.body));
47          return Response.json({ settings: wire.project.settings });
48        }
49        const review = wire.reviews.find((r) => path.includes(`/reviews/${r.id}/`))!;
50        if (path.endsWith("/receipt")) {
51          return review?.receipt?.digest
52            ? Response.json(fixtureOwnerReceipt(review))
53            : Response.json({ error: "Receipt not found." }, { status: 404 });
54        }
55        if (path.endsWith("/approve")) {
56          review.approval = { sha: review.sha, receiptDigest: review.receiptDigest };
57          return Response.json({ review });
58        }
59        if (path.endsWith("/promote")) {
60          if (wire.project.settings!.requireReceipt) {
61            review.receipt = fixtureReceipt("failed", "before-promote");
62            review.receiptDigest = review.receipt.digest;
63            return Response.json({ error: "Checks failed before promote." }, { status: 409 });
64          }
65          review.promoted = true;
66          return Response.json({ sha: review.sha });
67        }
68        return Response.json({ error: "Fixture missing route" }, { status: 404 });
69      }),
70  });
71  const app = mountDashboard(root, {
72    brand: { name: "Fixture product", tagline: "Fixture" },
73    fixture: true,
74    owner,
75    transport,
76    initialProject: id,
77  });
78  return {
79    dom,
80    root,
81    wire,
82    calls,
83    app,
84    close() {
85      app.dispose();
86      dom.window.close();
87    },
88  };
89}
90
91test("receipt wire maps every status without logs; durations and timeout use supplied check time", async () => {
92  for (const [ms, text] of [[0, "0 ms"], [1, "1 ms"], [227, "227 ms"], [999, "999 ms"], [1000, "1 s"], [1499, "1 s"], [1500, "2 s"]] as const)
93    assert.equal(duration(ms), text);
94  assert.equal(duration(-1), "");
95  assert.equal(duration(Number.NaN), "");
96  assert.equal(duration(41000), "41 s");
97  assert.equal(duration(110000), "1 min 50 s");
98  assert.equal(duration(60000), "1 min");
99  assert.equal(duration(59999), "1 min");
100  assert.equal(duration(null), "");
101  const c = setup();
102  try {
103    await until(() => !!c.root.querySelector("#reviews"));
104    const rows = [...c.root.querySelectorAll(".review-card")];
105    assert.equal(rows.length, 7);
106    for (const word of [
107      "Checks running",
108      "Re-checking",
109      "Passed",
110      "Failed",
111      "Timed out",
112      "Couldn't run",
113      "Not started",
114    ])
115      assert.ok(
116        rows.some((row) => row.textContent!.includes(word)),
117        word,
118      );
119    assert.match(rows[4].textContent!, /Stopped after 1 min 50 s · brand/);
120    assert.match(rows[5].textContent!, /The checks didn't start/);
121    assert.equal(
122      c.root.querySelectorAll('[data-action="approve"]').length,
123      0,
124      "only passing row is already approved",
125    );
126    assert.equal(JSON.stringify(c.wire).includes("stdout"), false);
127    assert.deepEqual(stateView(c.wire).reviews[4].receipt, c.wire.reviews[4].receipt);
128  } finally {
129    c.close();
130  }
131});
132
133test("queue and recent receipts use singular and plural check counts, including failed totals", async () => {
134  for (const count of [1, 2]) {
135    const c = setup("history-required");
136    try {
137      await until(() => !!c.root.querySelector(".receipt-history-row"));
138      for (const review of c.wire.reviews) {
139        if (review.receipt) {
140          review.receipt.checks = review.receipt.checks.slice(0, count);
141          review.receipt.durationMs = 4000;
142        }
143      }
144      await c.app.refresh();
145      const expected = `${count} ${count === 1 ? "check" : "checks"} · 4 s`;
146      await until(() =>
147        c.root.querySelector(".receipt-history-row")!.textContent!.includes(expected),
148      );
149      const passedReview = c.wire.reviews.find((review) => review.id === "receipt-3")!;
150      const passed = [...c.root.querySelectorAll(".review-card")].find((row) => row.textContent!.includes(passedReview.repoName))!;
151      assert.ok(passed.querySelector(".receipt-cell")!.textContent!.includes(expected));
152      const history = [...c.root.querySelectorAll(".receipt-history-row")];
153      assert.equal(history.length, 5);
154      assert.ok(history.every((row) => row.textContent!.includes(expected)));
155      const failed = c.root.querySelectorAll(".review-card")[3];
156      assert.ok(failed.textContent!.includes(`1 of ${count} failed`));
157      assert.doesNotMatch(c.root.textContent!, /1 checks/);
158    } finally {
159      c.close();
160    }
161  }
162});
163
164test("Promote stays hidden during an in-flight re-check and returns after an interrupted run", async () => {
165  const wire = fixtureState("receipts");
166  const review = wire.reviews[2];
167  let finish: (() => void) | undefined;
168  let promoteCalls = 0;
169  const c = setup("receipts", true, async (input) => {
170    const path = new URL(String(input)).pathname;
171    if (path.endsWith("/state")) return Response.json(wire);
172    if (path.endsWith("/activity")) return Response.json({ activity: [], nextAfter: 0 });
173    if (path.endsWith("/promote")) {
174      promoteCalls++;
175      await new Promise<void>((resolve) => {
176        finish = resolve;
177      });
178      review.receipt = fixtureReceipt("timed-out", "before-promote");
179      return Response.json({ error: "Checks failed before promote." }, { status: 409 });
180    }
181    return Response.json({ error: "Fixture missing route" }, { status: 404 });
182  });
183  const selector = '[data-action="promote"][data-review="receipt-3"]';
184  try {
185    await until(() => !!c.root.querySelector(selector));
186    c.root.querySelector<HTMLButtonElement>(selector)!.click();
187    await until(() => !!c.dom.window.document.querySelector('[data-action="confirm-promote"]'));
188    c.dom.window.document.querySelector('[data-action="confirm-promote"]')!.click();
189    await until(() => !!finish);
190    assert.equal(c.root.querySelector(selector), null);
191    const confirm = c.dom.window.document.querySelector(
192      '[data-action="confirm-promote"]',
193    ) as HTMLButtonElement;
194    assert.equal(confirm.textContent, "Re-checking…");
195    assert.equal(confirm.disabled, true);
196    assert.equal(promoteCalls, 1);
197    finish!();
198    await until(() => !!c.root.querySelector(selector));
199    assert.match(
200      c.root.querySelector(selector)!.closest("tr")!.textContent!,
201      /Promote runs the checks again/,
202    );
203    assert.equal(promoteCalls, 1, "interrupted runs are never automatically retried");
204  } finally {
205    finish?.();
206    c.close();
207  }
208});
209
210test("advisory receipt outcomes never gate; approved reviews re-satisfy the gate after setting flip", () => {
211  const state = stateView(fixtureState("advisory"));
212  for (const review of state.reviews) {
213    review.status = "pending";
214    assert.equal(reviewGate(review, state).approve, true);
215    review.status = "approved";
216    assert.equal(reviewGate(review, state).promote, true);
217  }
218  state.project.settings!.requireReceipt = true;
219  const failed = state.reviews[3];
220  failed.status = "pending";
221  assert.equal(reviewGate(failed, state).approve, false);
222  failed.status = "approved";
223  assert.equal(reviewGate(failed, state).promote, true, "promote enters core's fresh re-run");
224  failed.status = "stale";
225  assert.equal(reviewGate(failed, state).promote, false);
226  assert.equal(
227    receiptGateCaption(state.reviews[4], true),
228    "Checks timed out. A new push runs them again.",
229  );
230  const advisory = fixtureState("advisory");
231  advisory.reviews[3].approval = { sha: advisory.reviews[3].sha, receiptDigest: null };
232  assert.equal(
233    stateView(advisory).reviews[3].status,
234    "approved",
235    "advisory digest movement is not a moved fork",
236  );
237});
238
239test("owner logs use encoded owner route, last 200 lines, Show all; agent has no logs or switch", async () => {
240  const c = setup();
241  try {
242    await until(() => !!c.root.querySelector("#reviews"));
243    c.root.querySelectorAll<HTMLButtonElement>(".column-receipts .receipt-cell")[2].click();
244    await until(() => !!c.root.querySelector(".receipt-details"));
245    const logs = [...c.root.querySelectorAll<HTMLButtonElement>("button")].find(
246      (b) => b.textContent === "View logs",
247    )!;
248    logs.click();
249    await until(() => !!c.root.querySelector(".receipt-logs"));
250    assert.equal(c.root.querySelector(".receipt-logs")!.textContent!.split("\n").length, 200);
251    [...c.root.querySelectorAll<HTMLButtonElement>("button")]
252      .find((b) => b.textContent === "Show all")!
253      .click();
254    await until(() => c.root.querySelector(".receipt-logs")!.textContent!.split("\n").length > 200);
255    const hide = [...c.root.querySelectorAll<HTMLButtonElement>("button")].find(
256      (b) => b.textContent === "Hide logs",
257    )!;
258    hide.click();
259    await until(() => !c.root.querySelector(".receipt-logs"));
260    assert.ok(
261      [...c.root.querySelectorAll<HTMLButtonElement>("button")].some(
262        (b) => b.textContent === "View logs",
263      ),
264    );
265    assert.equal(c.calls.filter((call) => call.path.endsWith("/receipt")).length, 1);
266    assert.match(c.calls.at(-1)!.path, /\/projects\/receipts\/reviews\/receipt-3\/receipt$/);
267  } finally {
268    c.close();
269  }
270  const agent = setup("advisory", false);
271  try {
272    await until(() => !!agent.root.querySelector("#reviews"));
273    agent.root.querySelectorAll<HTMLButtonElement>(".column-receipts .receipt-cell")[2].click();
274    await until(() => !!agent.root.querySelector(".receipt-details"));
275    assert.doesNotMatch(agent.root.textContent!, /View logs/);
276    assert.equal(agent.root.querySelector('[role="switch"]'), null);
277    assert.match(agent.root.textContent!, /Require passing receipts: Off/);
278    assert.equal(
279      agent.calls.some((call) => call.path.endsWith("/receipt")),
280      false,
281    );
282  } finally {
283    agent.close();
284  }
285});
286
287test("required promote 409 keeps dialog with failure and opens new receipt; advisory has no re-check copy", async () => {
288  const c = setup();
289  try {
290    await until(() => !!c.root.querySelector('[data-action="promote"]'));
291    c.root
292      .querySelector<HTMLButtonElement>('[data-action="promote"][data-review="receipt-3"]')!
293      .click();
294    await until(() => !!c.dom.window.document.querySelector('[data-action="confirm-promote"]'));
295    assert.match(c.dom.window.document.body.textContent!, /Checks run again/);
296    c.dom.window.document.querySelector('[data-action="confirm-promote"]').click();
297    await until(() => c.dom.window.document.body.textContent!.includes("Nothing was promoted."));
298    assert.ok(c.dom.window.document.querySelector(".promotion-dialog"));
299    assert.equal(
300      c.root.querySelector('[role="alert"]'),
301      null,
302      "handled conflict has no page error banner",
303    );
304    assert.equal(c.dom.window.document.querySelectorAll('[role="alert"]').length, 1);
305    assert.doesNotMatch(c.dom.window.document.body.textContent!, /Checks failed before promote\./);
306    assert.ok(
307      c.dom.window.document.querySelector(".receipt-error-line svg"),
308      "Warning icon accompanies error copy",
309    );
310    assert.equal(c.root.querySelector('[data-action="promote"][data-review="receipt-3"]'), null);
311    assert.equal(c.calls.filter((call) => call.path.endsWith("/promote")).length, 1);
312    [...c.dom.window.document.querySelectorAll(".promotion-dialog button")]
313      .find((b: HTMLButtonElement) => b.textContent === "View receipt")!
314      .click();
315    await until(
316      () => c.root.querySelector(".receipt-details")?.textContent?.includes("Failed") === true,
317    );
318    assert.equal(
319      c.root.textContent!.includes("Fork moved"),
320      false,
321      "fresh re-run digest doesn't imply fork movement",
322    );
323  } finally {
324    c.close();
325  }
326  const a = setup("advisory");
327  try {
328    await until(() => !!a.root.querySelector('[data-action="approve"][data-review="receipt-4"]'));
329    a.root
330      .querySelector<HTMLButtonElement>('[data-action="approve"][data-review="receipt-4"]')!
331      .click();
332    await until(() => !!a.root.querySelector('[data-action="promote"][data-review="receipt-4"]'));
333    a.root
334      .querySelector<HTMLButtonElement>('[data-action="promote"][data-review="receipt-4"]')!
335      .click();
336    await until(() => !!a.dom.window.document.querySelector(".promotion-dialog"));
337    assert.match(
338      a.dom.window.document.body.textContent!,
339      /latest checks failed; receipts are advisory/,
340    );
341    assert.doesNotMatch(a.dom.window.document.body.textContent!, /Checks run again|Re-checking/);
342  } finally {
343    a.close();
344  }
345});
346
347test("settings PATCH sends only requireReceipt and reloads authoritative state", async () => {
348  const c = setup("advisory");
349  try {
350    await until(() => !!c.root.querySelector('[role="switch"]'));
351    c.root.querySelector<HTMLButtonElement>('[role="switch"]')!.click();
352    await until(
353      () =>
354        c.wire.project.settings!.requireReceipt &&
355        c.root.querySelector('[role="switch"]')!.getAttribute("aria-checked") === "true",
356    );
357    const patches = c.calls.filter((call) => call.method === "PATCH");
358    assert.equal(patches.length, 1);
359    assert.equal(patches[0].body, '{"requireReceipt":true}');
360    assert.ok(c.calls.at(-1)!.path.endsWith("/activity"));
361    assert.equal(c.root.querySelector('[data-action="approve"][data-review="receipt-5"]'), null);
362  } finally {
363    c.close();
364  }
365});
366
367test("late owner log response is discarded when receipt changes", async () => {
368  const dom = new JSDOM('<div id="dashboard"></div>', { url: origin });
369  installDom(dom.window);
370  const root = dom.window.document.getElementById("dashboard");
371  const react = createRoot(root);
372  const review = stateView(fixtureState("receipts")).reviews[2];
373  let finish!: (value: ReturnType<typeof fixtureOwnerReceipt>) => void;
374  const load = () =>
375    new Promise<ReturnType<typeof fixtureOwnerReceipt>>((resolve) => {
376      finish = resolve;
377    });
378  const draw = () =>
379    flushSync(() =>
380      react.render(
381        createElement(ReceiptDetails, { review, project: "receipts", owner: true, load }),
382      ),
383    );
384  try {
385    draw();
386    root.querySelector('button[aria-label="Copy to clipboard"]')?.blur();
387    [...root.querySelectorAll("button")]
388      .find((b: HTMLButtonElement) => b.textContent === "View logs")!
389      .click();
390    await until(() => !!finish);
391    const stored = fixtureOwnerReceipt(fixtureState("receipts").reviews[2]);
392    review.receipt = fixtureReceipt("running");
393    draw();
394    finish(stored);
395    await new Promise((resolve) => setTimeout(resolve, 20));
396    assert.equal(root.querySelector(".receipt-logs"), null);
397  } finally {
398    flushSync(() => react.unmount());
399    dom.window.close();
400  }
401});
402
403test("owner receipt and settings calls encode IDs, preserve session safeguards, and never retry", async () => {
404  const requests: { url: string; init?: RequestInit }[] = [];
405  const transport = createHttpTransport({
406    origin,
407    fetch: async (input, init) => {
408      requests.push({ url: String(input), init });
409      return Response.json({ error: "Receipt not found." }, { status: 404 });
410    },
411  });
412  await assert.rejects(transport.getReceipt!("a/b", "r/id"), /Receipt not found/);
413  await assert.rejects(
414    transport.setSettings!("a/b", { requireReceipt: false }),
415    /Receipt not found/,
416  );
417  assert.equal(requests.length, 2);
418  assert.equal(requests[0].url, `${origin}/projects/a%2Fb/reviews/r%2Fid/receipt`);
419  for (const request of requests) {
420    assert.equal(request.init!.credentials, "same-origin");
421    assert.equal(request.init!.cache, "no-store");
422    assert.equal(request.init!.redirect, "error");
423  }
424  assert.equal(requests[1].init!.method, "PATCH");
425  assert.equal(requests[1].init!.body, '{"requireReceipt":false}');
426  assert.deepEqual(requests[1].init!.headers, { "Content-Type": "application/json" });
427});
428
429test("receipt completion announces once after an observed running state", async () => {
430  const c = setup();
431  try {
432    await until(() => !!c.root.querySelector("#reviews"));
433    const live = c.root.querySelector<HTMLElement>('.sr-only[aria-live="polite"]')!;
434    assert.equal(live.textContent, "");
435    c.wire.reviews[0].receipt = fixtureReceipt("passed");
436    c.wire.reviews[0].receiptDigest = c.wire.reviews[0].receipt.digest;
437    await c.app.refresh();
438    await until(() => live.textContent!.includes("Checks passed for ui-01"));
439    let mutations = 0;
440    const observer = new c.dom.window.MutationObserver(() => mutations++);
441    observer.observe(live, { childList: true, subtree: true, characterData: true });
442    await c.app.refresh();
443    await new Promise((resolve) => setTimeout(resolve, 20));
444    assert.equal(mutations, 0, "identical snapshots don't repeat the announcement");
445    observer.disconnect();
446  } finally {
447    c.close();
448  }
449});
450
451test("approved before-promote outcomes distinguish failed code from an interrupted run", async () => {
452  const wire = fixtureState("gating-recheck");
453  const state = stateView(wire);
454  const running = state.reviews[1];
455  assert.equal(running.status, "approved");
456  assert.equal(receiptGateCaption(running, true), "");
457  const failed = state.reviews[2];
458  assert.equal(reviewGate(failed, state).promote, false);
459  assert.equal(receiptGateCaption(failed, true), "Checks failed. The agent must push a fix.");
460  for (const index of [4, 5]) {
461    assert.equal(reviewGate(state.reviews[index], state).promote, true);
462    assert.equal(receiptGateCaption(state.reviews[index], true), "Promote runs the checks again.");
463  }
464  state.project.settings!.requireReceipt = false;
465  assert.equal(
466    reviewGate(failed, state).promote,
467    true,
468    "advisory mode never gates on a failed run",
469  );
470  const c = setup("gating-recheck");
471  try {
472    await until(() => !!c.root.querySelector("#reviews"));
473    const rows = [...c.root.querySelectorAll(".review-card")];
474    assert.match(rows[1].textContent!, /Approved/);
475    assert.doesNotMatch(rows[1].textContent!, /Waiting for checks/);
476    assert.match(rows[1].textContent!, /Re-checking/);
477    assert.equal(rows[1].querySelector('[data-action="promote"]'), null);
478    assert.equal(rows[2].querySelector('[data-action="promote"]'), null);
479    assert.ok(rows[4].querySelector('[data-action="promote"]'));
480    assert.match(rows[4].textContent!, /Promote runs the checks again/);
481    assert.equal(rows[6].querySelectorAll(".state-reason").length, 1);
482    assert.equal((rows[6].textContent!.match(/Waiting for checks\./g) ?? []).length, 1);
483    assert.match(rows[6].querySelector(".column-receipts")!.textContent!, /Not started/);
484  } finally {
485    c.close();
486  }
487});
488
489test("recent receipts retain promoted summaries, sort by receipt time and cap at five without changing the queue", () => {
490  for (const id of ["history-required", "history-advisory"]) {
491    const state = stateView(fixtureState(id));
492    const order = state.reviews.map((r) => r.id);
493    const history = recentReceipts(state);
494    assert.deepEqual(
495      history.map((r) => r.id),
496      ["promoted-6", "promoted-5", "promoted-4", "promoted-3", "promoted-2"],
497    );
498    assert.ok(history.every((r) => r.status === "promoted"));
499    assert.deepEqual(
500      state.reviews.map((r) => r.id),
501      order,
502      "source order stays intact",
503    );
504    assert.equal(history[0].receipt.status, id === "history-required" ? "passed" : "failed");
505    const running = state.reviews[0];
506    running.receipt!.startedAt = "2026-10-12T02:00:00Z";
507    assert.equal(
508      recentReceipts(state)[0].id,
509      running.id,
510      "unfinished receipts order by their actual start",
511    );
512    for (const review of state.reviews) review.receipt = null;
513    assert.deepEqual(recentReceipts(state), []);
514  }
515});
516
517test("last promotion binds exact ledger review, SHA and digest, including advisory failures", () => {
518  const state = stateView(fixtureState("history-advisory"));
519  const entry = state.ledger.at(-1)!;
520  assert.equal(entry.receiptDigest, fixtureState("history-advisory").ledger.at(-1)!.receiptDigest);
521  const matching = promotionReceipt(state, entry)!;
522  assert.equal(matching.id, "promoted-6");
523  assert.equal(matching.receipt!.status, "failed");
524  for (const change of [
525    { receiptDigest: null },
526    { receiptDigest: undefined },
527    { receiptDigest: "9".repeat(64) },
528    { review: "missing" },
529    { sha: "9".repeat(40) },
530  ]) {
531    assert.equal(promotionReceipt(state, { ...entry, ...change }), undefined);
532  }
533  matching.receipt = { ...matching.receipt!, status: "running" };
534  assert.equal(promotionReceipt(state, entry), undefined);
535  matching.receipt = null;
536  assert.equal(promotionReceipt(state, entry), undefined);
537});
538
539test("promoted history and Last promotion open the same receipt with owner-only escaped logs", async () => {
540  for (const id of ["history-required", "history-advisory"]) {
541    const c = setup(id);
542    try {
543      await until(() => c.root.querySelectorAll(".receipt-history-row").length === 5);
544      assert.equal(c.root.querySelectorAll(".review-card").length, 7);
545      const rows = [...c.root.querySelectorAll<HTMLButtonElement>(".receipt-history-link")];
546      assert.match(rows[0].closest("tr")!.textContent!, /history-6.*7777777/);
547      assert.match(rows[0].closest("tr")!.textContent!, /3 checks · 41 s/);
548      assert.match(
549        rows[0].closest("tr")!.textContent!,
550        id === "history-required" ? /Before promote/ : /On review/,
551      );
552      assert.ok(rows[0].closest("tr")!.querySelector("time"));
553      rows[0].focus();
554      rows[0].click();
555      await until(
556        () => c.root.querySelector(".receipt-details")?.textContent?.includes("history-6") === true,
557      );
558      [...c.root.querySelectorAll<HTMLButtonElement>("button")]
559        .find((b) => b.textContent === "View logs")!
560        .click();
561      await until(() => !!c.root.querySelector(".receipt-logs"));
562      assert.equal(c.calls.filter((call) => call.path.endsWith("/receipt")).length, 1);
563      assert.match(c.calls.at(-1)!.path, /reviews\/promoted-6\/receipt$/);
564      assert.equal(c.root.querySelector(".receipt-logs script"), null);
565      assert.match(
566        c.root.querySelector(".receipt-logs")!.textContent!,
567        /<script>fixture<\/script>/,
568      );
569      c.root.querySelector<HTMLButtonElement>(".promotion-receipt")!.click();
570      await until(
571        () => c.root.querySelector(".receipt-details")?.textContent?.includes("history-6") === true,
572      );
573      assert.match(
574        c.root.querySelector(".promotion-receipt")!.textContent!,
575        id === "history-required" ? /Passed/ : /Failed/,
576      );
577      c.wire.ledger.at(-1)!.receiptDigest = "0".repeat(64);
578      await c.app.refresh();
579      await until(() => !c.root.querySelector(".promotion-receipt"));
580    } finally {
581      c.close();
582    }
583    const agent = setup(id, false);
584    try {
585      await until(() => !!agent.root.querySelector(".receipt-history-row"));
586      agent.root.querySelector<HTMLButtonElement>(".receipt-history-link")!.click();
587      await until(() => !!agent.root.querySelector(".receipt-details"));
588      assert.doesNotMatch(agent.root.textContent!, /View logs|Hide logs/);
589      assert.equal(
590        agent.calls.some((call) => call.path.endsWith("/receipt")),
591        false,
592      );
593    } finally {
594      agent.close();
595    }
596  }
597});
598
599test("empty history is honest even while receipt-free reviews exist", async () => {
600  const c = setup("quiet");
601  try {
602    await until(() => !!c.root.querySelector("#recent-receipts"));
603    assert.equal(c.root.querySelector("#recent-receipts p")!.textContent, "No receipts yet.");
604    assert.equal(c.root.querySelector(".receipt-history-row"), null);
605  } finally {
606    c.close();
607  }
608});
609
610test("receipt duration hours and relative time boundaries use singular days and whole hours", () => {
611  for (const [milliseconds, expected] of [
612    [3_599_499, "59 min 59 s"],
613    [3_599_500, "1 hr"],
614    [3_600_000, "1 hr"],
615    [3_660_000, "1 hr 1 min"],
616    [5_040_000, "1 hr 24 min"],
617    [86_400_000, "24 hr"],
618  ] as const)
619    assert.equal(duration(milliseconds), expected);
620  const start = "2026-10-12T00:00:00Z";
621  const epoch = Date.parse(start);
622  assert.equal(relativeTime(start, epoch + 60_000), "1 min ago");
623  assert.equal(relativeTime(start, epoch + 3_600_000), "1 hr ago");
624  assert.equal(relativeTime(start, epoch + 86_400_000), "1 day ago");
625  assert.equal(relativeTime(start, epoch + 172_800_000), "2 days ago");
626  assert.equal(relativeTime(start, epoch + 86_400_000, "hour"), "24 hr ago");
627});
628
629test("history has the queue's table headers, a packet control per row, and long-running relative copy", async () => {
630  const c = setup("history-required");
631  try {
632    await until(() => c.root.querySelectorAll(".receipt-history-row").length === 5);
633    assert.deepEqual(
634      [...c.root.querySelectorAll("#recent-receipts th")].map((h) => h.textContent),
635      ["Packet", "Head", "Outcome", "Checks", "Trigger", "Finished"],
636    );
637    assert.equal(c.root.querySelectorAll(".receipt-history-link").length, 5);
638    assert.equal(c.root.querySelector(".last-promotion-sha")!.tagName, "CODE");
639    assert.equal(c.root.querySelector(".last-promotion-sha")!.textContent, "7777777");
640    assert.match(
641      c.root.querySelector(".review-card .receipt-cell")!.textContent!,
642      /Started \d+ hr ago/,
643    );
644    assert.doesNotMatch(c.root.textContent!, /1 days ago|Started \d{3,} min/);
645    c.root.querySelectorAll<HTMLElement>(".receipt-history-row")[1].click();
646    await until(
647      () => c.root.querySelector(".receipt-details")?.textContent?.includes("history-5") === true,
648    );
649  } finally {
650    c.close();
651  }
652});
653