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

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

Snapshot at 34e3175

Containing directory

15,531 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 { reviewFixtureState, reviewFixtureDetail } from "./review-fixtures.ts";
8import { collisionFixtureState } from "./collision-fixtures.ts";
9import { judgeFixtureState, judgeFixtureDetail } from "./judge-fixtures.ts";
10import { emptyFixtureState } from "./empty-fixtures.ts";
11import type { StateResponse } from "../src/contracts.ts";
12const { JSDOM } = createRequire(import.meta.url)("jsdom") as {
13  JSDOM: new (html: string, options?: { url: string }) => {
14    window: Window & typeof globalThis & { close(): void };
15  };
16};
17const origin = "https://fixture.idiot.tax";
18const diagnosticId = "11111111-2222-4333-8444-555555555555";
19
20async function until(check: () => unknown) {
21  for (let i = 0; i < 250; i++) {
22    if (check()) return;
23    await new Promise((resolve) => setTimeout(resolve, 5));
24  }
25  assert.fail("Freshness fixture did not settle");
26}
27function deferred<T>() {
28  let resolve!: (value: T) => void;
29  const promise = new Promise<T>((done) => { resolve = done; });
30  return { promise, resolve };
31}
32function setup(wire: StateResponse, route = "review", owner = true, initialRead?: () => Promise<Response>) {
33  const dom = new JSDOM('<div id="app"></div>', { url: `${origin}/#${route}` });
34  installDom(dom.window);
35  const root = dom.window.document.getElementById("app")! as HTMLElement;
36  const calls: { path: string; method: string }[] = [];
37  let stateRead = initialRead;
38  let activityRead: (() => Promise<Response>) | undefined;
39  let detailRead: (() => Promise<Response>) | undefined;
40  let writeResponse: (() => Promise<Response>) | undefined;
41  const transport = createHttpTransport({ origin, fetch: async (input, init) => {
42    const path = new URL(String(input)).pathname;
43    calls.push({ path, method: init?.method ?? "GET" });
44    if (path.endsWith("/state")) return stateRead ? stateRead() : Response.json(wire);
45    if (path.endsWith("/activity")) return activityRead ? activityRead() : Response.json({ activity: wire.activity, nextAfter: 0 });
46    if (init?.method === "POST" || init?.method === "PATCH") return writeResponse ? writeResponse() : Response.json({});
47    const id = path.split("/").at(-1)!;
48    return detailRead ? detailRead() : Response.json(judgeFixtureDetail(wire, id));
49  }});
50  const app = mountDashboard(root, { transport, brand: { name: "Fixture product", tagline: "Fixture" },
51    fixture: true, owner, agent: "fixture-agent", initialProject: wire.project.id });
52  return { dom, root, wire, calls, app,
53    state(fn?: () => Promise<Response>) { stateRead = fn; },
54    activity(fn?: () => Promise<Response>) { activityRead = fn; },
55    detail(fn?: () => Promise<Response>) { detailRead = fn; },
56    write(fn?: () => Promise<Response>) { writeResponse = fn; },
57    close() { app.dispose(); dom.window.close(); },
58  };
59}
60const stamp = (c: ReturnType<typeof setup>) => c.root.querySelector<HTMLElement>('[data-freshness="stamp"]')!;
61const control = (c: ReturnType<typeof setup>, selector: string) => c.dom.window.document.querySelector<HTMLButtonElement>(selector)!;
62const writes = (c: ReturnType<typeof setup>) => c.calls.filter((call) => ["POST", "PATCH"].includes(call.method));
63async function failRefresh(c: ReturnType<typeof setup>) {
64  c.state(async () => Response.json({ error: "PRIVATE RAW STATE FAILURE" }, { status: 502 }));
65  await c.app.refresh();
66  assert.match(stamp(c).textContent!, /Couldn't refresh\. Last updated/);
67  assert.ok(stamp(c).querySelector("time")?.dateTime);
68  assert.doesNotMatch(c.root.textContent!, /PRIVATE RAW/);
69}
70function tryAgain(c: ReturnType<typeof setup>) {
71  return [...stamp(c).querySelectorAll("button")].find((button) => button.textContent === "Try again")!;
72}
73
74test("first read loading/failure has no stamp or first-run guidance; a successful empty retry does", async () => {
75  const pending = deferred<Response>();
76  const c = setup(emptyFixtureState("empty-agents"), "overview", true, () => pending.promise);
77  try {
78    assert.match(c.root.textContent!, /Loading the latest project snapshot…/);
79    assert.equal(c.root.querySelector("[data-first-run], [data-freshness]"), null);
80    pending.resolve(Response.json({ error: "Fixture first read failed." }, { status: 503 }));
81    await until(() => c.root.textContent!.includes("Fixture first read failed."));
82    assert.doesNotMatch(c.root.textContent!, /Loading the latest project snapshot|Last updated/);
83    assert.equal(c.root.querySelector("[data-first-run], [data-freshness]"), null);
84    c.state(undefined);
85    [...c.root.querySelectorAll("button")].find((button) => button.textContent === "Try again")!.click();
86    await until(() => c.root.querySelector('#view-overview [data-first-run="agents"]'));
87    assert.match(stamp(c).textContent!, /Updated just now/);
88  } finally { c.close(); }
89});
90
91test("in-flight refresh retains DOM, focus and enabled owner actions; failed activity fences cached data until retry", async () => {
92  const c = setup(reviewFixtureState("review-current"));
93  try {
94    await until(() => control(c, '[data-action="approve"]'));
95    const approve = control(c, '[data-action="approve"]');
96    approve.focus();
97    const table = c.root.querySelector("#reviews");
98    const updatedAt = stamp(c).querySelector("time")!.dateTime;
99    assert.match(stamp(c).textContent!, /Updated just now/);
100    const pending = deferred<Response>();
101    c.state(() => pending.promise);
102    const refreshing = c.app.refresh();
103    assert.equal(c.root.querySelector("#reviews"), table);
104    assert.equal(c.dom.window.document.activeElement, approve);
105    assert.equal(approve.disabled, false);
106    assert.equal(control(c, '[role="switch"]').disabled, false);
107    assert.equal(stamp(c).textContent, "Refreshing…");
108    assert.equal(stamp(c).querySelector('[role="status"], [aria-live]'), null);
109    assert.doesNotMatch(c.root.textContent!, /Loading the latest project snapshot/);
110    c.activity(async () => Response.json({ error: "PRIVATE RAW ACTIVITY FAILURE" }, { status: 502 }));
111    pending.resolve(Response.json(c.wire));
112    await refreshing;
113    assert.equal(c.root.querySelector("#reviews"), table);
114    assert.equal(stamp(c).querySelector("time")!.dateTime, updatedAt);
115    assert.equal(control(c, '[data-action="approve"]').disabled, true);
116    assert.equal(control(c, '[role="switch"]').disabled, true);
117    // Even scripted re-enabling cannot make stale handlers issue a write.
118    const staleApprove = control(c, '[data-action="approve"]');
119    staleApprove.disabled = false;
120    staleApprove.click();
121    const receiptSwitch = control(c, '[role="switch"]');
122    receiptSwitch.disabled = false;
123    receiptSwitch.click();
124    assert.equal(writes(c).length, 0);
125    c.state(undefined);
126    c.activity(undefined);
127    tryAgain(c).click();
128    await until(() => !control(c, '[data-action="approve"]').disabled && stamp(c).textContent!.includes("Updated"));
129    assert.equal(control(c, '[role="switch"]').disabled, false);
130  } finally { c.close(); }
131});
132
133test("owner can act during a read; a late failed read cannot stale or unlock the newer write", async () => {
134  const c = setup(reviewFixtureState("review-current"));
135  try {
136    await until(() => control(c, '[data-action="approve"]'));
137    const pending = deferred<Response>();
138    const pendingWrite = deferred<Response>();
139    c.state(() => pending.promise);
140    c.write(() => pendingWrite.promise);
141    const refreshing = c.app.refresh();
142    c.state(undefined);
143    control(c, '[data-action="approve"]').click();
144    assert.equal(writes(c).length, 1);
145    assert.equal(control(c, '[data-action="approve"]').disabled, true);
146    pending.resolve(Response.json({ error: "late failure" }, { status: 502 }));
147    await refreshing;
148    assert.doesNotMatch(stamp(c).textContent!, /Couldn't refresh/);
149    assert.equal(control(c, '[data-action="approve"]').disabled, true, "the older read cannot unlock the pending write");
150    pendingWrite.resolve(Response.json({}));
151    await until(() => !control(c, '[data-action="approve"]').disabled);
152    assert.equal(control(c, '[data-action="approve"]').disabled, false);
153    assert.equal(writes(c).length, 1, "no write retry");
154  } finally { c.close(); }
155});
156
157test("failed refresh disables ordinary and judge promotion without hiding them, including an open confirmation", async () => {
158  for (const judge of [false, true]) {
159    const c = setup(judge ? judgeFixtureState() : reviewFixtureState("review-approved"), judge ? "race/race-01" : "review");
160    const selector = judge ? '[data-action="promote-judge-pick"]' : '[data-action="promote"]';
161    try {
162      await until(() => control(c, selector));
163      const pending = deferred<Response>();
164      c.state(() => pending.promise);
165      const refreshing = c.app.refresh();
166      assert.equal(control(c, selector).disabled, false);
167      pending.resolve(Response.json(c.wire));
168      await refreshing;
169      control(c, selector).click();
170      await until(() => control(c, '[data-action="confirm-promote"]'));
171      await failRefresh(c);
172      assert.equal(control(c, selector).disabled, true);
173      const confirm = control(c, '[data-action="confirm-promote"]');
174      assert.equal(confirm.disabled, true);
175      confirm.disabled = false;
176      confirm.click();
177      assert.equal(writes(c).length, 0);
178      c.state(undefined);
179      await c.app.refresh();
180      await until(() => !control(c, selector).disabled);
181    } finally { c.close(); }
182  }
183});
184
185test("failed refresh disables Return and its open dialog; successful refresh enables both", async () => {
186  const c = setup(reviewFixtureState("review-current"), "review/review-1");
187  try {
188    await until(() => control(c, '[data-action="return"]'));
189    control(c, '[data-action="return"]').click();
190    await until(() => control(c, '[data-action="confirm-return"]'));
191    const textarea = c.dom.window.document.querySelector("textarea")!;
192    Object.getOwnPropertyDescriptor(c.dom.window.HTMLTextAreaElement.prototype, "value")!.set!.call(textarea, "Explain the changed behavior.");
193    textarea.dispatchEvent(new c.dom.window.Event("input", { bubbles: true }));
194    await until(() => !control(c, '[data-action="confirm-return"]').disabled);
195    await failRefresh(c);
196    assert.equal(control(c, '[data-action="return"]').disabled, true);
197    control(c, '[data-action="confirm-return"]').closest<HTMLElement>(".write-hint")!.focus();
198    await until(() => c.dom.window.document.body.textContent!.includes("Refresh first. This data is from"));
199    const confirm = control(c, '[data-action="confirm-return"]');
200    assert.equal(confirm.disabled, true);
201    confirm.disabled = false;
202    confirm.click();
203    assert.equal(writes(c).length, 0);
204    c.state(undefined);
205    await c.app.refresh();
206    await until(() => !control(c, '[data-action="return"]').disabled);
207    assert.equal(control(c, '[data-action="confirm-return"]').disabled, false);
208  } finally { c.close(); }
209});
210
211test("failed refresh fences Mark resolved, including keyboard submit on an already open dialog", async () => {
212  const c = setup(collisionFixtureState(), "work");
213  try {
214    await until(() => control(c, '[aria-label^="Mark resolved:"]'));
215    control(c, '[aria-label^="Mark resolved:"]').click();
216    await until(() => c.dom.window.document.querySelector(".collision-quick-reasons button"));
217    control(c, '.collision-quick-reasons button').click();
218    await failRefresh(c);
219    assert.equal(control(c, '[aria-label^="Mark resolved:"]').disabled, true);
220    const confirm = control(c, '[data-action="confirm-resolve"]');
221    assert.equal(confirm.disabled, true);
222    confirm.disabled = false;
223    confirm.click();
224    const textarea = c.dom.window.document.querySelector("textarea")!;
225    textarea.dispatchEvent(new c.dom.window.KeyboardEvent("keydown", { key: "Enter", ctrlKey: true, bubbles: true }));
226    assert.equal(writes(c).length, 0);
227    c.state(undefined);
228    await c.app.refresh();
229    await until(() => !control(c, '[data-action="confirm-resolve"]').disabled);
230  } finally { c.close(); }
231});
232
233test("stale agent connections keep owner controls hidden and retain the timestamp", async () => {
234  const c = setup(reviewFixtureState("review-current"), "review/review-1", false);
235  try {
236    await until(() => c.root.querySelector(".review-milestones"));
237    await failRefresh(c);
238    assert.equal(c.root.querySelector('[data-action="approve"], [data-action="promote"], [data-action="return"], [role="switch"]'), null);
239    c.state(undefined);
240    tryAgain(c).click();
241    await until(() => stamp(c).textContent!.includes("Updated"));
242    assert.equal(c.root.querySelector('[data-action="return"]'), null);
243  } finally { c.close(); }
244});
245
246test("slow review explanation appears after three seconds, then disappears on a successful read", async (t) => {
247  t.mock.timers.enable({ apis: ["setTimeout"] });
248  const pending = deferred<Response>();
249  const c = setup(reviewFixtureState("review-current"), "review/review-1");
250  c.detail(() => pending.promise);
251  try {
252    // Flush the initial state read/effect without advancing the review timer.
253    for (let i = 0; i < 20; i++) await Promise.resolve();
254    assert.match(c.root.textContent!, /Loading review…/);
255    assert.doesNotMatch(c.root.textContent!, /The first open/);
256    t.mock.timers.tick(2999);
257    assert.doesNotMatch(c.root.textContent!, /The first open/);
258    t.mock.timers.tick(1);
259    // React batches effects; flush without using the mocked timer queue.
260    await new Promise<void>((resolve) => setImmediate(resolve));
261    assert.match(c.root.textContent!, /The first open of a review reads its commit from the fork and can take up to 30 s\. Later opens are quick\./);
262    pending.resolve(Response.json(reviewFixtureDetail(c.wire)));
263    for (let i = 0; i < 20; i++) await Promise.resolve();
264    await new Promise<void>((resolve) => setImmediate(resolve));
265    assert.ok(c.root.querySelector(".review-milestones"));
266    assert.doesNotMatch(c.root.textContent!, /The first open/);
267  } finally { c.close(); t.mock.timers.reset(); }
268});
269
270test("review error never exposes raw bodies, retries one read, and shows bounded diagnostic IDs only to owners", async () => {
271  for (const owner of [true, false]) {
272    const c = setup(reviewFixtureState("review-current"), "review/review-1", owner);
273    let failed = true;
274    c.detail(async () => failed
275      ? Response.json({ error: "PRIVATE RAW 502 BODY", diagnosticId }, { status: 502 })
276      : Response.json(reviewFixtureDetail(c.wire)));
277    try {
278      await until(() => c.root.textContent!.includes("Couldn't load this review."));
279      assert.match(c.root.textContent!, /Reading the fork's repository failed\. Try again in a moment\./);
280      assert.doesNotMatch(c.root.textContent!, /PRIVATE RAW/);
281      assert.equal(c.root.textContent!.includes(`Diagnostic ID ${diagnosticId}`), owner);
282      const before = c.calls.filter((call) => call.path.endsWith("/reviews/review-1")).length;
283      failed = false;
284      [...c.root.querySelectorAll("button")].find((button) => button.textContent === "Try again")!.click();
285      await until(() => c.root.querySelector(".review-milestones"));
286      assert.equal(c.calls.filter((call) => call.path.endsWith("/reviews/review-1")).length, before + 1);
287      assert.doesNotMatch(c.root.textContent!, /Diagnostic ID/);
288    } finally { c.close(); }
289  }
290});
291