Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-dashboard/test/archive.test.ts
Snapshot at 34e3175
1import { installDom } from "./dom.ts";
2import assert from "node:assert/strict";
3import { createRequire } from "node:module";
4import { test } from "node:test";
5import { mountDashboard } from "../src/dashboard.ts";
6import { createHttpTransport } from "../src/transport.ts";
7import {
8 canArchive,
9 recentReceipts,
10 reviewQueue,
11 reviewState,
12 stateView,
13 workPackets,
14} from "../src/model.ts";
15import { ownerNeeds } from "../src/needs-you-model.ts";
16import { archiveFixtureState, archiveRace } from "./archive-fixtures.ts";
17import { reviewFixtureDetail } from "./review-fixtures.ts";
18const { JSDOM } = createRequire(import.meta.url)("jsdom");
19const brand = { name: "Fixture product", tagline: "Fixture" };
20async function until(check: () => boolean) {
21 for (let i = 0; i < 200; i++) {
22 if (check()) return;
23 await new Promise((resolve) => setTimeout(resolve, 5));
24 }
25 assert.fail("Archive DOM did not settle");
26}
27function setup(id = "archive-ready", owner = true, failure = 0, hash = "#work") {
28 const dom = new JSDOM('<div id="dashboard"></div>', { url: `https://fixture.idiot.tax/${hash}` });
29 installDom(dom.window);
30 const root = dom.window.document.getElementById("dashboard") as HTMLElement;
31 const state = archiveFixtureState(id);
32 const calls: string[] = [];
33 const transport = createHttpTransport({
34 origin: dom.window.location.origin,
35 fetch: async (input, init) => {
36 const path = new URL(String(input)).pathname;
37 calls.push(`${init?.method ?? "GET"} ${path}`);
38 if (path.endsWith("/state")) return Response.json(state);
39 if (path.endsWith("/activity"))
40 return Response.json({ activity: state.activity, nextAfter: 0 });
41 if (path.endsWith("/reviews/review-1")) return Response.json(reviewFixtureDetail(state));
42 if (/\/(unarchive|archive)$/.test(path)) {
43 if (failure === 409) state.races = [archiveRace()];
44 if (failure === 404) state.packets = state.packets.filter((p) => p.id !== "FH-040");
45 if (failure)
46 return Response.json({ error: "Raw server error forbidden" }, { status: failure });
47 const packet = state.packets[0];
48 packet.archived = path.endsWith("/unarchive")
49 ? null
50 : { at: "2026-10-13T15:00:00Z", by: "owner" };
51 return Response.json({ packet });
52 }
53 return Response.json({ error: "Missing fixture" }, { status: 404 });
54 },
55 });
56 const app = mountDashboard(root, { brand, transport, initialProject: id, fixture: true, owner });
57 const button = (action: string) =>
58 root.querySelector<HTMLButtonElement>(`button[data-action="${action}"]`);
59 async function open() {
60 await until(() => !!root.querySelector("#packets .packet-link"));
61 root.querySelector<HTMLButtonElement>("#packets .packet-link")!.click();
62 if (state.packets[0].mode === "race" && !state.packets[0].archived) {
63 await until(() => [...root.querySelectorAll("button")]
64 .some((entry) => entry.textContent === "Packet details"));
65 [...root.querySelectorAll<HTMLButtonElement>("button")]
66 .find((entry) => entry.textContent === "Packet details")!.click();
67 }
68 await until(() => !!root.querySelector(".archive-heading"));
69 }
70 return {
71 dom,
72 root,
73 state,
74 calls,
75 app,
76 button,
77 open,
78 close() {
79 app.dispose();
80 dom.window.close();
81 },
82 };
83}
84
85test("archive predicate mirrors raw core review, claim, race and both blocker identities", () => {
86 for (const id of [
87 "archive-claim",
88 "archive-race",
89 "archive-review",
90 "archive-main-moved",
91 "archive-blocker",
92 ]) {
93 const state = stateView(archiveFixtureState(id));
94 assert.equal(canArchive(state, "FH-040"), false, id);
95 if (id === "archive-main-moved")
96 assert.equal(reviewState(state.reviews[0], state).kind, "main-moved");
97 }
98 const wire = archiveFixtureState();
99 const r = wire.reviews[0];
100 const projected = stateView(wire);
101 delete projected.reviews[0].promoted;
102 assert.equal(projected.reviews[0].status, "promoted");
103 assert.equal(canArchive(projected, "FH-040"), false, "display promoted cannot close a raw-open review");
104 r.promoted = false;
105 r.approval = { sha: "9".repeat(40), receiptDigest: null };
106 assert.equal(stateView(wire).reviews[0].status, "stale");
107 assert.equal(canArchive(stateView(wire), "FH-040"), false, "display stale is still raw-open");
108 r.stale = true;
109 assert.equal(canArchive(stateView(wire), "FH-040"), true);
110 r.stale = false;
111 r.returned = { reason: "Try again", at: "2026-10-13T14:00:00Z" };
112 assert.equal(canArchive(stateView(wire), "FH-040"), true);
113 wire.races = [archiveRace("2026-10-13T14:00:00Z")];
114 assert.equal(canArchive(stateView(wire), "FH-040"), true);
115 assert.equal(canArchive(stateView(wire), "FH-READY"), true, "never started");
116 const blockerWire = archiveFixtureState("archive-blocker");
117 assert.equal(canArchive(stateView(blockerWire), "FH-READY"), false);
118 delete blockerWire.blockers[0].resolved;
119 assert.equal(canArchive(stateView(blockerWire), "FH-040"), false, "legacy blocker stays open");
120 blockerWire.blockers[0].resolved = { at: "2026-10-13T14:00:00Z", by: "owner" };
121 assert.equal(canArchive(stateView(blockerWire), "FH-040"), true);
122});
123
124test("archive filters Work, Review and Needs you but preserves receipts, ledger and activity", () => {
125 const wire = archiveFixtureState("archive-list");
126 const review = wire.reviews[0];
127 review.promoted = false;
128 review.returned = { reason: "History", at: "2026-10-13T14:00:00Z" };
129 const state = stateView(wire);
130 assert.equal(reviewQueue(state).length, 0);
131 assert.equal(ownerNeeds(state, brand).length, 0);
132 assert.deepEqual(
133 workPackets(state).map((p) => p.id),
134 ["FH-READY"],
135 );
136 assert.equal(recentReceipts(state).length, 1);
137 assert.deepEqual(
138 state.ledger.map((entry) => entry.sha),
139 wire.ledger.map((entry) => entry.after),
140 );
141 assert.deepEqual(state.recentActivity, wire.activity);
142 state.packets.push({
143 ...state.packets[0],
144 id: "older",
145 archived: { at: "2026-10-12T14:00:00Z", by: "owner" },
146 });
147 assert.deepEqual(
148 workPackets(state, true).map((p) => p.id),
149 ["FH-READY", "FH-040", "older"],
150 );
151 delete wire.packets[0].archived;
152 assert.equal(stateView(wire).packets[0].archived, null);
153});
154
155test("owner archives in the panel, retains focus, toggles neutral rows and undoes once", async () => {
156 const c = setup();
157 try {
158 await c.open();
159 const history = structuredClone({
160 reviews: c.state.reviews,
161 ledger: c.state.ledger,
162 activity: c.state.activity,
163 });
164 c.button("archive")!.focus();
165 c.button("archive")!.click();
166 c.button("archive")?.click();
167 await until(() => !!c.button("unarchive"));
168 assert.equal(c.dom.window.document.activeElement, c.button("unarchive"));
169 await until(() => c.dom.window.document.body.textContent.includes("Archived FH-040"));
170 assert.match(c.root.querySelector("#activity-panel")!.textContent!, /Archived .* by you/);
171 assert.equal(c.root.querySelectorAll("#packets tbody tr").length, 1);
172 assert.deepEqual(
173 { reviews: c.state.reviews, ledger: c.state.ledger, activity: c.state.activity },
174 history,
175 );
176 const packetsMetric = c.root.querySelector(".metrics p.metric-value")!;
177 assert.equal(packetsMetric.textContent, "1");
178 assert.equal(c.root.querySelector(".metrics h3"), null);
179 assert.equal(c.calls.filter((p) => p.startsWith("POST")).length, 1);
180 c.button("toggle-archived")!.click();
181 await until(() => c.root.querySelectorAll("#packets tbody tr").length === 2);
182 assert.equal(c.button("toggle-archived")!.getAttribute("aria-pressed"), "true");
183 const archivedRow = [...c.root.querySelectorAll("#packets tbody tr")].at(-1)!;
184 assert.match(archivedRow.textContent!, /FH-040.*Archived/);
185 assert.equal(archivedRow.querySelector(".status-dot"), null);
186 assert.match(c.root.textContent!, /1 archived packets shown/);
187 c.button("toggle-archived")!.click();
188 await until(() => c.root.querySelectorAll("#packets tbody tr").length === 1);
189 c.button("unarchive")!.click();
190 await until(() => !!c.button("archive"));
191 assert.equal(c.dom.window.document.activeElement, c.button("archive"));
192 assert.equal(c.root.querySelectorAll("#packets tbody tr").length, 2);
193 assert.equal(c.button("toggle-archived"), null);
194 await until(() => c.dom.window.document.body.textContent.includes("Unarchived FH-040"));
195 } finally {
196 c.close();
197 }
198});
199
200test("agents get the archived toggle and caption without archive or unarchive controls", async () => {
201 for (const id of ["archive-ready", "archive-list"]) {
202 const c = setup(id, false);
203 try {
204 if (id === "archive-list") {
205 await until(() => !!c.button("toggle-archived"));
206 c.button("toggle-archived")!.click();
207 await until(() => c.root.querySelectorAll("#packets .packet-link").length === 2);
208 [...c.root.querySelectorAll<HTMLButtonElement>("#packets .packet-link")].at(-1)!.click();
209 await until(() => !!c.root.querySelector(".archive-heading"));
210 assert.match(c.root.querySelector("#activity-panel")!.textContent!, /by the owner/);
211 } else await c.open();
212 assert.equal(c.button("archive"), null);
213 assert.equal(c.button("unarchive"), null);
214 assert.equal(c.calls.filter((p) => p.startsWith("POST")).length, 0);
215 } finally {
216 c.close();
217 }
218 }
219});
220
221test("all in-flight states hide panel Archive", async () => {
222 for (const id of [
223 "archive-claim",
224 "archive-race",
225 "archive-review",
226 "archive-main-moved",
227 "archive-blocker",
228 ]) {
229 const c = setup(id);
230 try {
231 await c.open();
232 assert.equal(c.button("archive"), null, id);
233 } finally {
234 c.close();
235 }
236 }
237});
238
239test("archive errors refresh once, sanitize copy, hide after conflict and close on missing", async () => {
240 for (const failure of [409, 404, 503]) {
241 const c = setup("archive-ready", true, failure);
242 try {
243 await c.open();
244 c.button("archive")!.click();
245 await until(() => !!c.root.querySelector('[role="alert"]'));
246 const message = c.root.querySelector('[role="alert"]')!.textContent!;
247 assert.doesNotMatch(message, /Raw server/);
248 assert.match(
249 message,
250 failure === 409
251 ? /FH-040 has work in flight/
252 : failure === 404
253 ? /This packet doesn't exist anymore/
254 : /Couldn't archive FH-040. Try again/,
255 );
256 assert.equal(c.calls.filter((p) => p.startsWith("POST")).length, 1);
257 assert.equal(c.calls.filter((p) => p.endsWith("/state")).length, 2);
258 if (failure === 409) assert.equal(c.button("archive"), null);
259 if (failure === 404) assert.equal(c.root.querySelector(".archive-heading"), null);
260 } finally {
261 c.close();
262 }
263 }
264});
265
266test("archived promoted review bookmark stays open with its neutral badge and evidence", async () => {
267 const c = setup("archive-list", true, 0, "#review/review-1");
268 try {
269 await until(() => !!c.root.querySelector(".review-header"));
270 assert.match(c.root.querySelector(".review-header")!.textContent!, /FH-040.*Archived/);
271 assert.match(c.root.querySelector(".review-detail")!.textContent!, /Promoted to main/);
272 } finally {
273 c.close();
274 }
275});
276
277test("archive transport encodes packet routes and uses the existing same-origin session once", async () => {
278 const calls: { url: string; init?: RequestInit }[] = [];
279 const transport = createHttpTransport({
280 origin: "https://fixture.idiot.tax",
281 fetch: async (input, init) => {
282 calls.push({ url: String(input), init });
283 return Response.json({ packet: archiveFixtureState().packets[0] });
284 },
285 });
286 await transport.archivePacket!("project/one", "packet/one");
287 await transport.unarchivePacket!("project/one", "packet/one");
288 assert.deepEqual(
289 calls.map((call) => new URL(call.url).pathname),
290 [
291 "/projects/project%2Fone/packets/packet%2Fone/archive",
292 "/projects/project%2Fone/packets/packet%2Fone/unarchive",
293 ],
294 );
295 for (const { init } of calls) {
296 assert.equal(init?.method, "POST");
297 assert.equal(init?.credentials, "same-origin");
298 assert.equal(init?.cache, "no-store");
299 assert.equal(init?.redirect, "error");
300 assert.equal(init?.body, "{}");
301 assert.equal(new Headers(init?.headers).has("Authorization"), false);
302 }
303});
304
305test("uncertain unarchive failure stays in the panel with sanitized copy and no retry", async () => {
306 const c = setup("archive-list", true, 503);
307 try {
308 await until(() => !!c.button("toggle-archived"));
309 c.button("toggle-archived")!.click();
310 await c.open();
311 // The active row opened by setup is the next packet; select archived history instead.
312 [...c.root.querySelectorAll<HTMLButtonElement>("#packets .packet-link")].at(-1)!.click();
313 await until(() => !!c.button("unarchive"));
314 c.button("unarchive")!.click();
315 await until(() => !!c.root.querySelector('[role="alert"]'));
316 assert.match(
317 c.root.querySelector('[role="alert"]')!.textContent!,
318 /Couldn't unarchive FH-040. Try again./,
319 );
320 assert.doesNotMatch(c.root.textContent!, /Raw server error/);
321 assert.equal(c.calls.filter((call) => call.startsWith("POST")).length, 1);
322 assert.ok(c.button("unarchive"));
323 } finally {
324 c.close();
325 }
326});
327
328
329test("finished race keeps its track and opens packet details for archive and undo", async () => {
330 const c = setup("archive-finished-race", true, 0, "#race/FH-040");
331 try {
332 await until(() => [...c.root.querySelectorAll("button")]
333 .some((button) => button.textContent === "Packet details"));
334 const details = [...c.root.querySelectorAll<HTMLButtonElement>("button")]
335 .find((button) => button.textContent === "Packet details")!;
336 assert.ok(details);
337 details.click();
338 await until(() => !!c.button("archive"));
339 assert.equal(c.dom.window.location.hash, "#work");
340 c.button("archive")!.click();
341 await until(() => !!c.button("unarchive"));
342 c.button("toggle-archived")!.click();
343 await until(() => c.root.querySelectorAll("#packets tbody tr").length === 2);
344 [...c.root.querySelectorAll<HTMLButtonElement>("#packets .packet-link")].at(-1)!.click();
345 assert.ok(c.button("unarchive"), "archived race row opens details directly");
346 assert.equal(c.root.querySelector(".race-page"), null);
347 c.button("unarchive")!.click();
348 await until(() => !!c.button("archive"));
349 } finally { c.close(); }
350});
351
352
353test("open race Work row routes both roles through the track to read-only live packet details", async () => {
354 for (const owner of [true, false]) {
355 const c = setup("archive-race", owner);
356 try {
357 await until(() => !!c.root.querySelector('#packets [data-packet-id="FH-040"]'));
358 const row = c.root.querySelector('#packets [data-packet-id="FH-040"]')!;
359 assert.match(row.textContent!, /Race/);
360 assert.match(row.textContent!, /Racing/);
361 assert.match(row.textContent!, /1 entrant/);
362 row.querySelector<HTMLButtonElement>(".packet-link")!.click();
363 await until(() => !!c.root.querySelector(".race-navigation"));
364 assert.equal(c.dom.window.location.hash, "#race/FH-040");
365 const buttons = c.root.querySelectorAll<HTMLButtonElement>(".race-navigation button");
366 assert.equal(buttons.length, 2);
367 assert.match(buttons[0].textContent!, /Work/);
368 assert.equal(buttons[1].textContent, "Packet details");
369 buttons[1].click();
370 await until(() => !!c.root.querySelector(".archive-heading"));
371 assert.equal(c.button("archive"), null);
372 assert.equal(c.button("unarchive"), null);
373 assert.equal(c.calls.filter((call) => call.startsWith("POST")).length, 0);
374 } finally { c.close(); }
375 }
376});
377
378test("finished archive race fixture records a promoted winner and closed other entrants", () => {
379 const wire = archiveFixtureState("archive-finished-race");
380 const race = wire.races![0];
381 const winner = race.entrants.find((entrant) => entrant.agent === race.winner)!;
382 assert.equal(wire.packets[0].mode, "race");
383 assert.equal(winner.promotedAt, race.finishedAt);
384 assert.equal(winner.place, 1);
385 assert.equal(winner.closedAt, race.finishedAt);
386 assert.equal(winner.closedBy, null);
387 assert.equal(race.ledgerEntryId, wire.ledger[0].id);
388 assert.equal(wire.reviews[0].agent, winner.agent);
389 assert.ok(wire.agents.some((agent) => agent.name === winner.agent));
390 assert.ok(race.entrants.filter((entrant) => entrant !== winner)
391 .every((entrant) => entrant.closedBy === "race-finished" && entrant.closedAt === race.finishedAt));
392 assert.equal(canArchive(stateView(wire), "FH-040"), true);
393});
394