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

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

Snapshot at 34e3175

Containing directory

16,702 bytes · typescriptView raw
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