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