Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/receipts.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 { 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