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

packages/forkhorse-dashboard/test/needs-you.test.ts

Snapshot at 34e3175

Containing directory

11,202 bytes · typescriptView raw
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 { reviewState, stateView } from "../src/model.ts";
7import { ownerNeeds, copyText } from "../src/needs-you-model.ts";
8import { createHttpTransport } from "../src/transport.ts";
9import { needsFixtureState } from "./needs-you-fixtures.ts";
10import { fixtureReceipt } from "./fixtures.ts";
11
12const brand = { name: "Fixture product", tagline: "Offline" };
13const { JSDOM } = createRequire(import.meta.url)("jsdom");
14const now = Date.parse("2026-10-13T12:10:00Z");
15const kinds = ["promotionPending", "reconciler", "blocker", "failed", "promote", "approve"];
16const needs = (id = "needs-all") => ownerNeeds(stateView(needsFixtureState(id)), brand, now);
17
18test("six public state kinds retain evidence and exact priority without duplicating pending promotion", () => {
19  const wire = needsFixtureState("needs-all");
20  const state = stateView(wire);
21  assert.deepEqual(state.promotionPending, wire.promotionPending);
22  assert.equal(state.reconcilerAlerts.length, 1);
23  assert.equal(state.reconcilerAlerts[0].ref, "refs/heads/main");
24  const items = ownerNeeds(state, brand, now);
25  assert.deepEqual(
26    items.map((item) => item.kind),
27    kinds,
28  );
29  assert.match(copyText(items[0].why), /Fixture product started moving main aaaaaaa → bbbbbbb/);
30  assert.match(
31    copyText(items[2].why),
32    /fixture-reviewer pushed bbbbbbb to src\/claims.ts \+1, which fixture-builder has claimed/,
33  );
34  assert.equal(copyText(items[3].why), "1 of 3 checks failed · brand (13 s)");
35  assert.equal(copyText(items[4].outcome), "main moves aaaaaaa → ddddddd. Checks run again first.");
36  assert.equal(copyText(items[5].why), "Requested 10 min ago · Checks passed");
37  assert.equal(needs("needs-empty").length, 0);
38});
39
40test("oldest first uses durable receive sequence and review request time, independent of lexical wire order", () => {
41  const state = stateView(needsFixtureState("needs-all"));
42  const alert = state.reconcilerAlerts[0];
43  state.reconcilerAlerts = [
44    { ...alert, id: "10" },
45    { ...alert, id: "2" },
46  ];
47  const blocker = state.blockers[0];
48  state.blockers = [
49    { ...blocker, id: "10-p" },
50    { ...blocker, id: "2-p" },
51  ];
52  const review = state.reviews.at(-1)!;
53  state.reviews.push({ ...review, id: "earlier", requestedAt: "2026-10-12T12:00:00Z" });
54  const items = ownerNeeds(state, brand, now);
55  assert.deepEqual(
56    items.filter((i) => i.kind === "reconciler").map((i) => i.id),
57    ["alert:2", "alert:10"],
58  );
59  assert.deepEqual(
60    items.filter((i) => i.kind === "blocker").map((i) => i.id),
61    ["blocker:2-p", "blocker:10-p"],
62  );
63  assert.deepEqual(
64    items.filter((i) => i.kind === "approve").map((i) => i.id),
65    ["approve:earlier", `approve:${review.id}`],
66  );
67});
68
69test("stale, moved, returned, promoted and blocked reviews never claim readiness", () => {
70  for (const mode of [
71    "stale",
72    "moved",
73    "returned",
74    "promoted",
75    "blocked",
76    "missing-settings",
77  ] as const) {
78    const state = stateView(needsFixtureState("needs-approve"));
79    const review = state.reviews[0];
80    if (mode === "stale") review.status = "stale";
81    if (mode === "moved") state.forks[0].head = "9".repeat(40);
82    if (mode === "returned") review.returned = { reason: "Fix it", at: "2026-10-13T12:00:00Z" };
83    if (mode === "promoted") review.status = "promoted";
84    if (mode === "blocked")
85      state.blockers.push({
86        id: "1-p",
87        packet: "claimant-packet",
88        pushingPacket: review.packet,
89        repoName: review.repoName,
90        agent: "other",
91        paths: [],
92        reason: "Collision",
93        resolved: false,
94      });
95    if (mode === "missing-settings") delete state.project.settings;
96    assert.ok(
97      !ownerNeeds(state, brand).some((i) => i.kind === "approve" || i.kind === "promote"),
98      mode,
99    );
100    review.receipt = fixtureReceipt("failed");
101    if (["stale", "moved", "returned", "promoted"].includes(mode))
102      assert.equal(ownerNeeds(state, brand).length, 0, mode);
103  }
104});
105
106test("required interrupted checks get one evidence row, advisory failures stay ready to review", () => {
107  for (const status of ["failed", "timed-out", "infrastructure-error"] as const) {
108    const state = stateView(needsFixtureState("needs-promote"));
109    state.reviews[0].receipt = fixtureReceipt(status, "before-promote");
110    const items = ownerNeeds(state, brand);
111    assert.deepEqual(
112      items.map((i) => i.kind),
113      ["failed"],
114    );
115    assert.match(
116      copyText(items[0].outcome),
117      status === "failed" ? /pushes a fix/ : /Promote or a new push/,
118    );
119  }
120  assert.deepEqual(
121    needs("needs-advisory").map((i) => i.kind),
122    ["approve"],
123  );
124  assert.match(copyText(needs("needs-advisory")[0].why), /Checks advisory/);
125  const state = stateView(needsFixtureState("needs-failed"));
126  state.reviews[0].receipt!.checks = [state.reviews[0].receipt!.checks[0]];
127  assert.match(copyText(ownerNeeds(state, brand)[0].why), /^1 of 1 check failed/);
128});
129
130test("FH-045 derived Main moved excludes the nonstale approved loser and keeps the ready review", () => {
131  const wire = needsFixtureState("needs-main-moved");
132  assert.equal(wire.reviews[0].stale, false);
133  assert.ok(wire.reviews[0].approval);
134  const state = stateView(wire);
135  assert.equal(reviewState(state.reviews[0], state).kind, "main-moved");
136  assert.equal(reviewState(state.reviews[1], state).kind, "current");
137  const items = ownerNeeds(state, brand, now);
138  assert.deepEqual(
139    items.map((item) => copyText(item.title)),
140    ["Promote fixture-ready to main"],
141  );
142  state.reviews[0].receipt = fixtureReceipt("failed");
143  assert.deepEqual(
144    ownerNeeds(state, brand, now).map((item) => item.id),
145    [items[0].id],
146  );
147  state.reviews[1].stale = true;
148  assert.equal(reviewState(state.reviews[1], state).kind, "out-of-date");
149  assert.equal(ownerNeeds(state, brand, now).length, 0);
150});
151
152async function until(check: () => boolean) {
153  for (let i = 0; i < 200; i++) {
154    if (check()) return;
155    await new Promise((resolve) => setTimeout(resolve, 5));
156  }
157  assert.fail("Needs you DOM did not settle");
158}
159function setup(id = "needs-all", owner = true, detail = true) {
160  const dom = new JSDOM('<div id="dashboard"></div>', {
161    url: "https://fixture.idiot.tax/#overview",
162  });
163  installDom(dom.window);
164  const root = dom.window.document.getElementById("dashboard")! as HTMLElement;
165  const wire = needsFixtureState(id);
166  const calls: { method: string; path: string }[] = [];
167  const transport = createHttpTransport({
168    origin: "https://fixture.idiot.tax",
169    fetch: async (input, init) => {
170      const path = new URL(String(input)).pathname;
171      calls.push({ method: init?.method ?? "GET", path });
172      if (path.endsWith("/state")) return Response.json(wire);
173      if (path.endsWith("/activity")) return Response.json({ activity: [], nextAfter: 0 });
174      return Response.json(
175        { error: "Synthetic detail intentionally unavailable" },
176        { status: 404 },
177      );
178    },
179  });
180  if (!detail) delete transport.getReview;
181  let invalidate: (() => void) | undefined;
182  transport.subscribe = (_project, listener) => {
183    invalidate = listener;
184    return () => {};
185  };
186  const app = mountDashboard(root, { brand, fixture: true, owner, initialProject: id, transport });
187  return {
188    dom,
189    root,
190    wire,
191    calls,
192    app,
193    invalidate: () => invalidate?.(),
194    close() {
195      app.dispose();
196      dom.window.close();
197    },
198  };
199}
200
201test("owner section is above project, semantic, limited to five, expandable and contains only evidence controls", async () => {
202  const c = setup();
203  try {
204    await until(() => !!c.root.querySelector(".needs-you-card"));
205    const card = c.root.querySelector(".needs-you-card")!;
206    assert.equal(card.parentElement!.getAttribute("aria-labelledby"), "needs-you-title");
207    assert.equal(card.parentElement!.nextElementSibling!.classList.contains("project-card"), true);
208    assert.equal(card.querySelectorAll("ul > li").length, 5);
209    assert.equal(
210      card.querySelectorAll('[data-action="approve"], [data-action="promote"]').length,
211      0,
212    );
213    const expand = [...card.querySelectorAll("button")].find(
214      (b) => b.textContent === "Show all 6",
215    )!;
216    expand.click();
217    await until(() => card.querySelectorAll("ul > li").length === 6);
218    for (const row of card.querySelectorAll(".needs-you-row")) {
219      assert.match(row.getAttribute("aria-label")!, /\./);
220      assert.equal(row.querySelectorAll('svg[aria-hidden="true"]').length, 2);
221    }
222    (card.querySelector('[data-need="approve"] button') as HTMLButtonElement).click();
223    await until(() => c.dom.window.location.hash.includes("#review/"));
224    assert.ok(c.calls.every((call) => call.method === "GET"));
225  } finally {
226    c.close();
227  }
228});
229
230test("queue-only host routes card to review queue without a dead detail hash", async () => {
231  const c = setup("needs-approve", true, false);
232  try {
233    await until(() => !!c.root.querySelector(".needs-you-row"));
234    (c.root.querySelector(".needs-you-row") as HTMLButtonElement).click();
235    assert.equal(c.dom.window.location.hash, "#review");
236    assert.equal(c.root.querySelector("#view-review")!.hasAttribute("hidden"), false);
237    assert.ok(c.calls.every((call) => call.method === "GET"));
238  } finally {
239    c.close();
240  }
241});
242
243test("collision opens Work details and reconciler routes to the matching agent row with focus", async () => {
244  const c = setup();
245  try {
246    await until(() => !!c.root.querySelector(".needs-you-row"));
247    (c.root.querySelector('[data-need="blocker"] button') as HTMLButtonElement).click();
248    assert.equal(c.dom.window.location.hash, "#work");
249    await until(
250      () => !!c.root.querySelector("#activity-panel")?.textContent?.includes("Collision evidence"),
251    );
252    (c.root.querySelector('[data-need="reconciler"] button') as HTMLButtonElement).click();
253    await until(
254      () =>
255        c.dom.window.document.activeElement?.getAttribute("data-agent-repo") ===
256        "fixture-project--external",
257    );
258    assert.equal(c.dom.window.location.hash, "#agents");
259  } finally {
260    c.close();
261  }
262});
263
264test("live invalidation removes resolved work to persistent empty card; agents have no card or shell", async () => {
265  const c = setup("needs-approve");
266  try {
267    await until(() => !!c.root.querySelector(".needs-you-row"));
268    c.wire.reviews[0].returned = { reason: "Fixture fix", at: "2026-10-13T12:00:00Z" };
269    c.invalidate();
270    await until(() => !!c.root.querySelector(".needs-you-empty"));
271    assert.equal(
272      c.root.querySelector(".needs-you-empty")!.textContent,
273      "Nothing needs you right now.",
274    );
275    assert.equal(c.root.querySelectorAll(".needs-you-row").length, 0);
276  } finally {
277    c.close();
278  }
279  const agent = setup("needs-all", false);
280  try {
281    await until(() => !!agent.root.querySelector(".project-card"));
282    assert.equal(agent.root.querySelector('[aria-labelledby="needs-you-title"]'), null);
283  } finally {
284    agent.close();
285  }
286});
287