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

packages/forkhorse-dashboard/test/fixtures.ts

Snapshot at 34e3175

Containing directory

13,359 bytes · typescriptView raw
1import type {
2  Activity,
3  ProjectState,
4  StateResponse,
5  ReceiptSummary,
6  ReceiptStatus,
7  OwnerReceipt,
8} from "../src/contracts.ts";
9
10export const HEAD = "a".repeat(40);
11export const NEXT_HEAD = "b".repeat(40);
12export const DIGEST = "d".repeat(64);
13
14/** Fictional fixture data. Neither this nor fixture routes ship in production assets. */
15function fixtureData(id = "demo"): ProjectState {
16  if (id === "quiet")
17    return {
18      project: { id, repoName: id, defaultBranch: "main", settings: { requireReceipt: true } },
19      canonicalHead: HEAD,
20      agents: [],
21      forks: [],
22      packets: [],
23      claims: [],
24      blockers: [],
25      reviews: [],
26      ledger: [],
27      reconcilerAlerts: [],
28    };
29  return {
30    project: { id, repoName: id, defaultBranch: "main", settings: { requireReceipt: true } },
31    canonicalHead: HEAD,
32    agents: [{ name: "builder" }, { name: "reviewer" }, { name: "docs" }],
33    forks: [
34      { agent: "builder", repoName: "demo--builder-1", head: NEXT_HEAD },
35      { agent: "reviewer", repoName: "demo--reviewer-1", head: "c".repeat(40) },
36      { agent: "docs", repoName: "demo--docs-1", head: "e".repeat(40) },
37    ],
38    packets: [
39      {
40        id: "ui-01",
41        title: "Project navigation & owner review",
42        goal: "Keep scoped work and its evidence together, from claim to promotion.",
43        paths: ["src/dashboard/**"],
44        acceptance: ["Phone and desktop layouts render", "Approve uses the review endpoint"],
45      },
46      {
47        id: "api-02",
48        title: "Claim conflict detection",
49        goal: "Raise a blocker when an agent pushes into another live claim.",
50        paths: ["src/claims/**"],
51        acceptance: ["Conflicting path creates a blocker"],
52      },
53      {
54        id: "docs-03",
55        title: "Owner setup guide",
56        goal: "Describe deployment configuration and the evidence required for release.",
57        paths: ["docs/**"],
58        acceptance: ["Examples use fictional fixture values"],
59      },
60    ],
61    claims: [
62      {
63        packet: "ui-01",
64        agent: "builder",
65        repoName: "demo--builder-1",
66        paths: ["src/dashboard/**"],
67        active: true,
68      },
69      {
70        packet: "api-02",
71        agent: "reviewer",
72        repoName: "demo--reviewer-1",
73        paths: ["src/claims/**"],
74        active: true,
75      },
76      {
77        packet: "docs-03",
78        agent: "docs",
79        repoName: "demo--docs-1",
80        paths: ["docs/**"],
81        active: true,
82      },
83    ],
84    blockers: [
85      {
86        id: "block-1",
87        agent: "reviewer",
88        packet: "api-02",
89        paths: ["src/claims/index.ts"],
90        reason: "Push overlaps a path held by another agent's live claim.",
91        resolved: false,
92      },
93    ],
94    reviews: [
95      {
96        id: "review-1",
97        repoName: "demo--builder-1",
98        agent: "builder",
99        packet: "ui-01",
100        sha: NEXT_HEAD,
101        receiptDigest: DIGEST,
102        status: "pending",
103        receipt: fixtureReceipt("passed"),
104      },
105      {
106        id: "review-2",
107        repoName: "demo--docs-1",
108        agent: "docs",
109        packet: "docs-03",
110        sha: "f".repeat(40),
111        receiptDigest: "1".repeat(64),
112        status: "stale",
113        receipt: fixtureReceipt("passed"),
114      },
115      {
116        id: "review-3",
117        repoName: "demo--reviewer-1",
118        agent: "reviewer",
119        packet: "api-02",
120        sha: "c".repeat(40),
121        receiptDigest: null,
122        status: "pending",
123      },
124    ],
125    ledger: [],
126    reconcilerAlerts: [],
127  };
128}
129
130export function fixtureActivity(): Activity[] {
131  return [
132    {
133      sequence: 1,
134      namespace: "fixture",
135      repoName: "demo--builder-1",
136      agent: "builder",
137      ref: "refs/heads/main",
138      before: HEAD,
139      after: NEXT_HEAD,
140      changedPaths: ["src/dashboard/index.ts"],
141      eventTimestamp: "2026-10-12T00:10:00.000Z",
142      recordedAt: "2026-10-12T00:10:01.000Z",
143    },
144    {
145      sequence: 2,
146      namespace: "fixture",
147      repoName: "demo--reviewer-1",
148      agent: "reviewer",
149      ref: "refs/heads/main",
150      before: HEAD,
151      after: "c".repeat(40),
152      changedPaths: ["src/claims/index.ts"],
153      eventTimestamp: "2026-10-12T00:12:00.000Z",
154      recordedAt: "2026-10-12T00:12:01.000Z",
155    },
156    {
157      sequence: 3,
158      namespace: "fixture",
159      repoName: "demo--docs-1",
160      agent: "docs",
161      ref: "refs/heads/main",
162      before: HEAD,
163      after: "e".repeat(40),
164      changedPaths: ["docs/setup.md"],
165      eventTimestamp: "2026-10-12T00:15:00.000Z",
166      recordedAt: "2026-10-12T00:15:01.000Z",
167    },
168  ];
169}
170
171/** Fixture API emits the FH-005 wire contract, not the view's normalized projection. */
172export function fixtureState(id = "demo"): StateResponse {
173  const data = fixtureData(id);
174  const createdAt = "2026-10-12T00:00:00.000Z";
175  const result: StateResponse = {
176    project: {
177      ...data.project,
178      namespace: "fixture",
179      events: "active",
180      remote: `https://git.idiot.tax/fixture/${id}.git`,
181    },
182    agents: data.agents,
183    forks: data.forks.map((fork, index) => ({
184      events: index === 0 ? "active" : "pending",
185      ...fork,
186      remote: `https://git.idiot.tax/fixture/${fork.repoName}.git`,
187      createdAt,
188    })),
189    packets: data.packets,
190    claims: data.claims.map(({ active: _active, ...claim }) => ({ ...claim, createdAt })),
191    blockers: data.blockers.map((blocker) => ({
192      id: blocker.id,
193      packet: blocker.packet,
194      claimingAgent: "builder",
195      pushingAgent: blocker.agent,
196      repoName: "demo--reviewer-1",
197      sha: "c".repeat(40),
198      paths: blocker.paths,
199    })),
200    reviews: data.reviews.map(({ status, ...review }) => ({
201      ...review,
202      base: HEAD,
203      approval:
204        status === "approved" ? { sha: review.sha, receiptDigest: review.receiptDigest } : null,
205      stale: status === "stale",
206      promoted: status === "promoted",
207      createdAt,
208    })),
209    ledger: [],
210    activity: id === "quiet" ? [] : fixtureActivity(),
211    reconcilerAlerts: [],
212    promotionPending: null,
213  };
214  if (["large", "truncated"].includes(id)) {
215    const agent = "oauth-aee4061c-representative-long-agent-name";
216    result.agents[0].name = agent;
217    result.forks[0].agent = agent;
218    result.claims[0].agent = agent;
219    result.reviews[0].agent = agent;
220    result.activity[0].agent = agent;
221  }
222  if (id === "superseded") {
223    const stale = structuredClone(result.reviews[0]);
224    stale.id = "superseded-stale";
225    stale.stale = true;
226    stale.sha = "9".repeat(40);
227    stale.createdAt = "2026-10-11T00:00:00Z";
228    result.reviews[0].promoted = true;
229    result.reviews = [result.reviews[0], stale];
230  }
231  if (["large", "first", "partial", "truncated", "deleted"].includes(id)) {
232    const paths = Array.from(
233      { length: 450 },
234      (_, i) => `src/imported/assets/group-${i}/very-long-filename-for-path-overflow-check-${i}.ts`,
235    );
236    for (const packet of result.packets) packet.paths = paths;
237    for (const claim of result.claims) claim.paths = paths;
238    for (const blocker of result.blockers) blocker.paths = paths;
239    for (const activity of result.activity) {
240      activity.changedPaths =
241        id === "partial" ? paths.slice(0, 30) : id === "truncated" ? paths.slice(0, 50) : paths;
242      if (id === "first") activity.before = "0".repeat(40);
243      if (id === "deleted") {
244        activity.after = "0".repeat(40);
245        activity.ref = "refs/heads/topic";
246      }
247      if (id === "truncated") {
248        activity.changedPathCount = 205;
249        activity.changedPathsTruncated = true;
250      }
251      if (id === "partial") {
252        activity.changedPathCount = 450;
253        activity.changedPathsTruncated = true;
254      }
255    }
256  }
257  if (
258    [
259      "receipts",
260      "advisory",
261      "recheck",
262      "rerun-failed",
263      "gating-recheck",
264      "history-required",
265      "history-advisory",
266    ].includes(id)
267  ) {
268    const statuses: (ReceiptStatus | null)[] = [
269      "running",
270      "running",
271      "passed",
272      "failed",
273      "timed-out",
274      "infrastructure-error",
275      null,
276    ];
277    result.project.settings = { requireReceipt: id !== "advisory" && id !== "receipts" };
278    result.blockers = [];
279    result.claims = [];
280    result.packets = [];
281    result.forks = [];
282    result.reviews = statuses.map((status, index) => {
283      const sha = String(index + 1).repeat(40);
284      const packet = `ui-${String(index + 1).padStart(2, "0")}`;
285      const agent = `builder-${index + 1}`;
286      const repoName = `${id}--${agent}-1`;
287      result.packets.push({
288        id: packet,
289        title: `Receipt ${packet}`,
290        goal: "Inspect fixture checks",
291        paths: ["src/dashboard/**"],
292        acceptance: ["Receipt state shown"],
293      });
294      result.forks.push({
295        agent,
296        repoName,
297        head: sha,
298        remote: `https://git.idiot.tax/fixture/${repoName}.git`,
299        createdAt,
300      });
301      const receipt = status
302        ? fixtureReceipt(status, index === 1 ? "before-promote" : "review-created")
303        : null;
304      return {
305        id: `receipt-${index + 1}`,
306        packet,
307        agent,
308        repoName,
309        sha,
310        base: HEAD,
311        receiptDigest: receipt?.digest ?? null,
312        receipt,
313        approval:
314          index === 2 || (index === 1 && id !== "advisory")
315            ? { sha, receiptDigest: index === 1 ? DIGEST : (receipt?.digest ?? null) }
316            : null,
317        stale: false,
318        promoted: false,
319        createdAt,
320      };
321    });
322    // A truthful required gate fixture also carries the two queue running trigger variants.
323    if (id === "receipts") result.project.settings.requireReceipt = true;
324    if (id === "advisory") result.reviews[1].receipt!.trigger = "review-created";
325    if (id === "gating-recheck") {
326      for (const index of [2, 4, 5]) {
327        const review = result.reviews[index];
328        review.receipt = fixtureReceipt(
329          index === 2 ? "failed" : index === 4 ? "timed-out" : "infrastructure-error",
330          "before-promote",
331        );
332        review.receiptDigest = review.receipt.digest;
333        review.approval = { sha: review.sha, receiptDigest: DIGEST };
334      }
335    }
336  }
337  if (id === "history-required" || id === "history-advisory") {
338    result.project.settings = { requireReceipt: id === "history-required" };
339    for (const review of result.reviews) {
340      if (review.receipt?.status === "running") review.receipt.startedAt = "2026-10-12T00:00:00Z";
341    }
342    const source = result.reviews[2];
343    // Shuffled history spans repeated packet IDs and both promotion modes.
344    const promoted = Array.from({ length: 7 }, (_, index) => {
345      const receipt = fixtureReceipt(
346        index === 6 && id === "history-advisory" ? "failed" : "passed",
347        id === "history-required" ? "before-promote" : "review-created",
348      );
349      receipt.digest = String(index + 1).repeat(64);
350      receipt.finishedAt = `2026-10-12T01:0${index}:41Z`;
351      const review = {
352        ...structuredClone(source),
353        id: `promoted-${index}`,
354        packet: `history-${index}`,
355        sha: String(index + 1).repeat(40),
356        receipt,
357        receiptDigest: receipt.digest,
358        approval: null,
359        promoted: true,
360      };
361      result.ledger.push({
362        id: `promotion-${index}`,
363        review: review.id,
364        repoName: id,
365        ref: "refs/heads/main",
366        before: HEAD,
367        after: review.sha,
368        receiptDigest: receipt.digest,
369        recordedAt: receipt.finishedAt,
370      });
371      return review;
372    });
373    result.reviews.push(...promoted.toReversed());
374  }
375  return result;
376}
377
378export function fixtureReceipt(
379  status: ReceiptStatus,
380  trigger: ReceiptSummary["trigger"] = "review-created",
381): ReceiptSummary {
382  return {
383    status,
384    trigger,
385    digest: status === "running" ? null : DIGEST,
386    startedAt:
387      status === "running" ? new Date(Date.now() - 12000).toISOString() : "2026-10-12T00:00:00Z",
388    finishedAt: status === "running" ? null : "2026-10-12T00:00:41Z",
389    durationMs: status === "running" ? null : 41000,
390    checks: [
391      fixtureCheck(
392        "brand",
393        status,
394        status === "running" ? null : status === "timed-out" ? 110000 : 13000,
395      ),
396      fixtureCheck(
397        "types",
398        status === "running" ? "running" : "passed",
399        status === "running" ? null : 14000,
400      ),
401      fixtureCheck(
402        "tests",
403        status === "running" ? "running" : "passed",
404        status === "running" ? null : 14000,
405      ),
406    ],
407  };
408}
409export function fixtureOwnerReceipt(review: StateResponse["reviews"][number]): OwnerReceipt {
410  return {
411    digest: review.receipt!.digest!,
412    receipt: {
413      commit: review.sha,
414      checks: review.receipt!.checks.map((check) => ({
415        check: { name: check.name },
416        logs: {
417          stdout: Array.from(
418            { length: 220 },
419            (_, index) =>
420              `${check.name} fixture output ${index + 1}${index === 219 ? " <script>fixture</script>" : ""}`,
421          ).join("\n"),
422          stderr: "Fixture stderr",
423          truncated: false,
424        },
425      })),
426    },
427  };
428}
429
430function fixtureCheck(name: string, outcome: ReceiptStatus, durationMs: number | null) {
431  return { name, tier: "full" as const, outcome, durationMs };
432}
433