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

packages/forkhorse-runner/__tests__/quick.test.ts

Snapshot at 34e3175

Containing directory

21,684 bytes · typescriptView raw
1import { env } from "cloudflare:workers";
2import { runInDurableObject } from "cloudflare:test";
3import { ReceiptRunner } from "./worker";
4import { afterEach, describe, expect, it, vi } from "vitest";
5import { parseChecks } from "../src/checks";
6import { QUICK_TIMEOUT_MS } from "../src/quick";
7import { Snapshot, QUICK_READ_BYTES, QUICK_LIST_PATHS } from "../src/snapshot";
8import { runReviewChecks, requirePassingReceipt, receiptJson, sha256 } from "../src/runner";
9import type { Check, QuickCheck } from "../src/types";
10import { check, COMMIT, fixture, IMAGE, processResult, request, review } from "./fixtures";
11// Vite loads the actual repo check unchanged into the real Dynamic Worker.
12// @ts-expect-error raw source is provided by Vite, not the Worker type library.
13import largeFilesSource from "../../../.forkhorse/quick/large-files.js?raw";
14// @ts-expect-error raw manifest is provided by Vite.
15import checksSource from "../../../.forkhorse/checks?raw";
16
17const quick: QuickCheck = { name: "quick", tier: "quick", module: ".forkhorse/quick/check.js" };
18const ROOT = "c".repeat(40), DIR = "d".repeat(40), CHECKS = "e".repeat(40), QUICK = "f".repeat(40), MODULE = "1".repeat(40), FILE = "2".repeat(40);
19function quickFixture(source = 'export default async ctx => ({ok: (await ctx.read("brand.json")) === "fixture"});', checks: Check[] = [quick]) {
20  const f = fixture();
21  f.repo.readTree.mockImplementation(async hash => {
22    if (hash === ROOT) return [{ name: ".forkhorse", type: "tree", mode: "40000", hash: DIR }, { name: "brand.json", type: "blob", mode: "100644", hash: FILE }];
23    if (hash === DIR) return [{ name: "checks", type: "blob", mode: "100644", hash: CHECKS }, { name: "quick", type: "tree", mode: "40000", hash: QUICK }];
24    if (hash === QUICK) return [{ name: "check.js", type: "blob", mode: "100644", hash: MODULE }];
25    throw new Error("provider-private-tree-error");
26  });
27  f.repo.readBlob.mockImplementation(async (...args: unknown[]) => new Blob([args[0] === CHECKS ? JSON.stringify(checks) : args[0] === MODULE ? source : "fixture"]));
28  return f;
29}
30
31async function run(f: ReturnType<typeof quickFixture>) {
32  const ns = (env as unknown as { RECEIPT_RUNNER: DurableObjectNamespace<ReceiptRunner> }).RECEIPT_RUNNER;
33  return runInDurableObject(ns.get(ns.newUniqueId()), async instance => {
34    f.ports.quick = (instance as ReceiptRunner).fixturePorts();
35    return runReviewChecks(request, f.ports);
36  });
37}
38
39describe("registered large-files full tier with explicitly mocked containers under workerd", () => {
40  it("registers one checkout command in the default image and keeps the example unregistered", () => {
41    const checks = parseChecks(checksSource);
42    expect(checks.filter(entry => entry.tier === "quick")).toEqual([
43      {name:"brand-quick",tier:"quick",module:".forkhorse/quick/brand.js"},
44    ]);
45    const large = checks.find(entry => entry.name === "large-files");
46    expect(large).toMatchObject({tier:"full",image:"default"});
47    expect(large && large.tier === "full" && large.run.startsWith("node -e ")).toBe(true);
48  });
49  it.each([0, 1])("dispatches the exact registered full command and retains exit %s", async exitCode => {
50    const large = parseChecks(checksSource).find(entry => entry.name === "large-files")!;
51    const f = fixture();
52    f.repo.readBlob.mockResolvedValue(new Blob([JSON.stringify([large])]));
53    Object.assign(f.container.images, {default:IMAGE});
54    const original = f.container.exec.getMockImplementation()!;
55    f.container.exec.mockImplementation(async (argv, options) => argv[0] === "timeout"
56      ? processResult(exitCode === 0 ? "All files are below 10 MB." : "", exitCode === 1 ? "fixture file is too large" : "", exitCode)
57      : original(argv, options));
58    const record = await runReviewChecks(request, f.ports);
59    expect(record.receipt.evidence).toBe("mocked-container-port");
60    expect(record.receipt.checks[0]).toMatchObject({runtime:"container",exitStatus:exitCode,outcome:exitCode === 0 ? "passed" : "failed"});
61    expect(f.container.exec.mock.calls.find(([argv]) => argv[0] === "timeout")?.[0])
62      .toEqual(["timeout", "--kill-after=5", "110", "sh", "-c", large.tier === "full" ? large.run : ""]);
63  });
64});
65
66describe("real Dynamic Worker loader in local workerd; Artifacts and containers are fixtures", () => {
67  afterEach(() => vi.useRealTimers());
68  it("runs a quick-only check with exact-commit token-free loopback reads and module provenance", async () => {
69    const f = quickFixture();
70    f.ports.container = undefined;
71    const record = await run(f);
72    expect(record.receipt).toMatchObject({ evidence: "dynamic-worker", checks: [{ runtime: "dynamic-worker", outcome: "passed" }] });
73    expect(record.receipt.checks[0]?.imageDigest).toMatch(/^module:sha256:[a-f0-9]{64}$/);
74    expect(f.repo.readCommit.mock.calls.every(([sha]) => sha === COMMIT)).toBe(true);
75    expect(f.repo.createToken).not.toHaveBeenCalled();
76    expect(f.container.start).not.toHaveBeenCalled();
77  });
78  it.each([
79    ['export default async () => ({ok:false,message:"bad brand"});', "failed", "bad brand"],
80    ['export default async () => { throw new Error("private-provider-stack-token"); };', "failed", "Quick check threw or returned an invalid result."],
81    ['export default ???', "infrastructure-error", "Quick worker could not start."],
82    ['throw new Error("private-provider-stack-token"); export default async () => ({ok:true});', "infrastructure-error", "Quick worker could not start."],
83  ])("classifies and sanitizes module %s", async (source, outcome, message) => {
84    const f = quickFixture(source);
85    const record = await run(f);
86    expect(record.receipt.checks[0]).toMatchObject({ outcome, message });
87    expect(JSON.stringify(record)).not.toContain("private-provider-stack-token");
88  });
89  it("blocks fetch and TCP network and supplies only FILES", async () => {
90    const f = quickFixture(`import {connect} from "cloudflare:sockets";
91      export default async () => {
92        let blocked = 0;
93        try { await fetch("https://network.idiot.tax/"); } catch { blocked++; }
94        try { const s = connect({hostname:"network.idiot.tax",port:443}); await s.opened; } catch { blocked++; }
95        return {ok:blocked === 2};
96      };`);
97    const record = await run(f);
98    expect(record.receipt.checks[0]?.outcome).toBe("passed");
99  });
100  it("cannot import the parent environment", async () => {
101    const f = quickFixture('import {env} from "cloudflare:workers"; export default async () => { try { return {ok:!!env.FILES}; } catch { return {ok:false}; } };');
102    const record = await run(f);
103    expect(record.receipt.checks[0]?.outcome).toBe("failed");
104  });
105  it("runs quick first, skips every full check on failure without starting or minting", async () => {
106    const f = quickFixture('export default async () => ({ok:false});', [check, quick]);
107    const record = await run(f);
108    expect(record.receipt.checks.map(r => r.check.tier)).toEqual(["quick", "full"]);
109    expect(record.receipt.checks[1]).toMatchObject({ outcome: "failed", durationMs: null, message: "Skipped: a quick check failed." });
110    expect(f.container.start).not.toHaveBeenCalled();
111    expect(f.repo.createToken).not.toHaveBeenCalled();
112  });
113  it("runs mixed suites while retaining mocked-container evidence", async () => {
114    const f = quickFixture('export default async ctx => ({ok:(await ctx.list(".forkhorse/quick/")).join() === ".forkhorse/quick/check.js"});', [check, quick]);
115    const record = await run(f);
116    expect(record.receipt.evidence).toBe("mocked-container-port");
117    expect(record.receipt.checks.map(r => r.outcome)).toEqual(["passed", "passed"]);
118    expect(f.container.start).toHaveBeenCalledOnce();
119  });
120  it("bounds UTF-8 messages and removes control escapes", async () => {
121    const f = quickFixture('export default async () => ({ok:false,message:"\\u001b" + "🐎".repeat(4000)});');
122    const record = await run(f);
123    const message = record.receipt.checks[0]!.message!;
124    expect(new TextEncoder().encode(message).length).toBeLessThanOrEqual(4096);
125    expect(message).not.toContain("\u001b");
126    expect(message).not.toContain("�");
127  });
128  it("enforces the real 10-second deadline and blocks full execution", async () => {
129    const f = quickFixture('export default async () => { await new Promise(r => setTimeout(r, 60000)); return {ok:true}; };', [quick, check]);
130    const start = Date.now();
131    const record = await run(f);
132    expect(record.receipt.checks[0]?.outcome).toBe("timed-out");
133    expect(Date.now() - start).toBeGreaterThanOrEqual(QUICK_TIMEOUT_MS - 100);
134    expect(Date.now() - start).toBeLessThan(QUICK_TIMEOUT_MS + 3000);
135    expect(f.container.start).not.toHaveBeenCalled();
136  }, 15_000);
137  it("accepts exact quick proof in required mode, rejects altered source/runtime and fake loaders; advisory ignores receipts", async () => {
138    const f = quickFixture();
139    const record = await run(f);
140    const expected = { ...review, checks: [quick], digest: record.digest,
141      moduleDigests: { [quick.module]: record.receipt.checks[0]!.imageDigest } };
142    await expect(requirePassingReceipt(record, expected)).resolves.toBeUndefined();
143    await expect(requirePassingReceipt(record, { ...expected, moduleDigests: {} })).rejects.toThrow();
144    await expect(requirePassingReceipt(null, { ...expected, requireReceipt: false })).resolves.toBeUndefined();
145    for (const mutation of ["runtime", "evidence", "digest", "commit"] as const) {
146      const altered = structuredClone(record);
147      if (mutation === "runtime") altered.receipt.checks[0]!.runtime = "container";
148      if (mutation === "evidence") altered.receipt.evidence = "mocked-worker-loader";
149      if (mutation === "digest") altered.receipt.checks[0]!.imageDigest = `module:sha256:${"0".repeat(64)}`;
150      if (mutation === "commit") altered.receipt.commit = "f".repeat(40);
151      altered.digest = await sha256(receiptJson(altered.receipt));
152      await expect(requirePassingReceipt(altered, { ...expected, digest: altered.digest })).rejects.toThrow();
153    }
154  });
155});
156
157describe("snapshot bounds with fixture Artifacts under workerd", () => {
158  it.each(["../brand.json", "/brand.json", "a//b", "a/./b", "a/../b", "a\\b", ".git/config", "a\0b"])("rejects unsafe path %s", async path => {
159    const f = quickFixture();
160    await expect(new Snapshot(f.repo, COMMIT).read(path)).rejects.toThrow("Snapshot read unavailable");
161    expect(f.repo.readCommit).not.toHaveBeenCalled();
162  });
163  it("accounts aggregate and concurrent UTF-8 reads, including repeated reads", async () => {
164    const f = quickFixture();
165    f.repo.readBlob.mockResolvedValue(new Blob(["🐎".repeat(QUICK_READ_BYTES / 8)]));
166    const snapshot = new Snapshot(f.repo, COMMIT);
167    const results = await Promise.allSettled([snapshot.read("brand.json"), snapshot.read("brand.json"), snapshot.read("brand.json")]);
168    expect(results.filter(r => r.status === "fulfilled")).toHaveLength(2);
169    expect(results.filter(r => r.status === "rejected")).toHaveLength(1);
170  });
171  it("bounds paths cumulatively and sanitizes provider errors", async () => {
172    const f = quickFixture();
173    f.repo.readTree.mockResolvedValue(Array.from({length: QUICK_LIST_PATHS}, (_, i) => ({ name: `f${i}`, type: "blob", mode: "100644", hash: FILE })));
174    const snapshot = new Snapshot(f.repo, COMMIT);
175    expect(await snapshot.list("")).toHaveLength(QUICK_LIST_PATHS);
176    await expect(snapshot.list("")).rejects.toThrow("Snapshot list unavailable or limit exceeded.");
177    f.repo.readTree.mockRejectedValue(new Error("secret-provider-body"));
178    await expect(new Snapshot(f.repo, COMMIT).read("f")).rejects.toThrow(/^Snapshot read unavailable or limit exceeded\.$/);
179  });
180  it("returns null for absent files and rejects symlinks and malformed UTF-8", async () => {
181    const f = quickFixture();
182    expect(await new Snapshot(f.repo, COMMIT).read("missing")).toBeNull();
183    f.repo.readBlob.mockResolvedValue(new Blob([new Uint8Array([255])]));
184    await expect(new Snapshot(f.repo, COMMIT).read("brand.json")).rejects.toThrow();
185    f.repo.readTree.mockResolvedValue([{ name: "brand.json", type: "symlink", mode: "120000", hash: FILE }]);
186    await expect(new Snapshot(f.repo, COMMIT).read("brand.json")).rejects.toThrow();
187  });
188  it("revokes pending reads and prevents later access", async () => {
189    const f = quickFixture();
190    let resolve!: (blob: Blob) => void;
191    let started!: () => void;
192    const reading = new Promise<void>(r => { started = r; });
193    f.repo.readBlob.mockImplementation(() => new Promise(r => { resolve = r; started(); }));
194    const snapshot = new Snapshot(f.repo, COMMIT);
195    const pending = snapshot.read("brand.json");
196    await reading;
197    snapshot.close(); resolve(new Blob(["private"]));
198    await expect(pending).rejects.toThrow();
199    await expect(snapshot.list("")).rejects.toThrow();
200  });
201  it("parses only the exact quick manifest contract", () => {
202    expect(parseChecks(JSON.stringify([quick]))).toEqual([quick]);
203    for (const bad of [{...quick,run:"x"}, {...quick,image:"x"}, {...quick,module:".forkhorse/quick/../bad.js"}, {...quick,module:"other.js"}])
204      expect(() => parseChecks(JSON.stringify([bad]))).toThrow();
205  });
206});
207
208describe("stat metadata in local workerd with fixture Artifacts", () => {
209  it("exposes stat through the real FILES loopback", async () => {
210    const f = quickFixture(`export default async ctx => {
211      const stat = await ctx.stat("brand.json");
212      return {ok:stat.size === 7 && stat.binary === false && await ctx.stat("missing") === null};
213    };`);
214    expect((await run(f)).receipt.checks[0]?.outcome).toBe("passed");
215    expect(f.repo.createToken).not.toHaveBeenCalled();
216  });
217  it.each([0, 8191, 8192])("detects NUL only within the first 8 KiB (offset %s)", async offset => {
218    const f = quickFixture();
219    const bytes = new Uint8Array(9000).fill(255); // Invalid UTF-8 is valid stat input.
220    bytes[offset] = 0;
221    const blob = new Blob([bytes]);
222    const slice = vi.spyOn(blob, "slice");
223    const wholeRead = vi.spyOn(blob, "arrayBuffer");
224    f.repo.readBlob.mockResolvedValue(blob);
225    expect(await new Snapshot(f.repo, COMMIT).stat("brand.json")).toEqual({size:9000,binary:offset < 8192});
226    expect(slice).toHaveBeenCalledExactlyOnceWith(0, 8192);
227    expect(wholeRead).not.toHaveBeenCalled();
228  });
229  it("handles empty and executable files and absent paths", async () => {
230    const f = quickFixture();
231    expect(await new Snapshot(f.repo, COMMIT).stat("missing")).toBeNull();
232    f.repo.readTree.mockResolvedValue([{name:"brand.json",type:"exec",mode:"100755",hash:FILE}]);
233    f.repo.readBlob.mockResolvedValue(new Blob([]));
234    expect(await new Snapshot(f.repo, COMMIT).stat("brand.json")).toEqual({size:0,binary:false});
235  });
236  it.each(["../brand.json", "/brand.json", "a//b", "a/./b", "a/../b", "a\\b", ".git/config", "a\0b", "", "a/", "a/.GIT/b", "a\u007fb"])("rejects unsafe stat path %s", async path => {
237    const f = quickFixture();
238    await expect(new Snapshot(f.repo, COMMIT).stat(path)).rejects.toThrow(/^Snapshot stat unavailable or limit exceeded\.$/);
239    expect(f.repo.readCommit).not.toHaveBeenCalled();
240  });
241  it.each(["symlink", "gitlink", "tree"] as const)("rejects non-regular %s entries", async type => {
242    const f = quickFixture();
243    f.repo.readTree.mockResolvedValue([{name:"brand.json",type,mode:type === "symlink" ? "120000" : type === "gitlink" ? "160000" : "40000",hash:FILE}]);
244    await expect(new Snapshot(f.repo, COMMIT).stat("brand.json")).rejects.toThrow();
245    expect(f.repo.readBlob).not.toHaveBeenCalled();
246  });
247  it("fails closed on missing objects and sanitizes provider exceptions", async () => {
248    const f = quickFixture();
249    f.repo.readBlob.mockResolvedValue(null as unknown as Blob);
250    await expect(new Snapshot(f.repo, COMMIT).stat("brand.json")).rejects.toThrow(/^Snapshot stat unavailable or limit exceeded\.$/);
251    f.repo.readBlob.mockRejectedValue(new Error("private-provider-body"));
252    await expect(new Snapshot(f.repo, COMMIT).stat("brand.json")).rejects.toThrow(/^Snapshot stat unavailable or limit exceeded\.$/);
253  });
254  it("shares the read budget with concurrent stat samples", async () => {
255    const f = quickFixture();
256    f.repo.readBlob.mockResolvedValue(new Blob(["x".repeat(QUICK_READ_BYTES - 8192)]));
257    const snapshot = new Snapshot(f.repo, COMMIT);
258    await snapshot.read("brand.json");
259    const results = await Promise.allSettled([snapshot.stat("brand.json"), snapshot.stat("brand.json")]);
260    expect(results.filter(r => r.status === "fulfilled")).toHaveLength(1);
261    expect(results.filter(r => r.status === "rejected")).toHaveLength(1);
262  });
263  it("reuses validated immutable tree metadata for a repository-wide scan", async () => {
264    const f = quickFixture();
265    f.repo.readTree.mockResolvedValue(Array.from({length:300}, (_, i) => ({name:`f${i}`,type:"blob",mode:"100644",hash:FILE})));
266    f.repo.readBlob.mockResolvedValue(new Blob(["x"]));
267    const snapshot = new Snapshot(f.repo, COMMIT);
268    const paths = await snapshot.list("");
269    const stats = await Promise.all(paths.map(path => snapshot.stat(path)));
270    expect(stats).toHaveLength(300);
271    expect(stats.every(stat => stat?.size === 1 && !stat.binary)).toBe(true);
272    expect(f.repo.readTree).toHaveBeenCalledOnce();
273  });
274  it("rejects fetched trees beyond the entry budget", async () => {
275    const f = quickFixture();
276    f.repo.readTree.mockResolvedValue(Array.from({length:10_001}, (_, i) => ({name:`f${i}`,type:"blob",mode:"100644",hash:FILE})));
277    await expect(new Snapshot(f.repo, COMMIT).stat("f0")).rejects.toThrow();
278    expect(f.repo.readBlob).not.toHaveBeenCalled();
279  });
280  it("revokes pending stat calls and fences closed capabilities", async () => {
281    const f = quickFixture();
282    let resolve!: (blob: Blob) => void;
283    let started!: () => void;
284    const reading = new Promise<void>(r => { started = r; });
285    f.repo.readBlob.mockImplementation(() => new Promise(r => { resolve = r; started(); }));
286    const snapshot = new Snapshot(f.repo, COMMIT);
287    const pending = snapshot.stat("brand.json");
288    await reading;
289    snapshot.close(); resolve(new Blob(["private"]));
290    await expect(pending).rejects.toThrow();
291    await expect(snapshot.stat("brand.json")).rejects.toThrow();
292  });
293  it.each([9_999_999, 10_000_000, 10_000_001])("runs the unregistered stat example at %s bytes without materializing the file", async size => {
294    const f = quickFixture(largeFilesSource);
295    const original = f.repo.readBlob.getMockImplementation()!;
296    const blob = new Blob([new Uint8Array(size)]);
297    const wholeRead = vi.spyOn(blob, "arrayBuffer");
298    f.repo.readBlob.mockImplementation(async hash => hash === FILE ? blob : original(hash));
299    const result = (await run(f)).receipt.checks[0]!;
300    expect(result.outcome).toBe(size < 10_000_000 ? "passed" : "failed");
301    if (size >= 10_000_000) {
302      expect(result.message).toContain(`brand.json: ${size} bytes`);
303      expect(result.message).toContain("32 MB");
304      expect(result.message).toContain("docs/large-files.md");
305    }
306    expect(wholeRead).not.toHaveBeenCalled();
307    expect(f.container.start).not.toHaveBeenCalled();
308  });
309});
310
311it("enforces the aggregate read budget through the real loopback and sanitizes caught provider errors", async () => {
312  const f = quickFixture(`export default async ctx => {
313    try { await ctx.read("brand.json"); await ctx.read("brand.json"); return {ok:true}; }
314    catch (error) { return {ok:false,message:error.message}; }
315  };`);
316  const original = f.repo.readBlob.getMockImplementation()!;
317  f.repo.readBlob.mockImplementation(async hash => hash === FILE ? new Blob(["x".repeat(QUICK_READ_BYTES / 2)]) : original(hash));
318  const record = await run(f);
319  expect(record.receipt.checks[0]).toMatchObject({outcome:"failed",message:"Snapshot read unavailable or limit exceeded."});
320});
321
322it("revokes the file capability and aborts dispatched fetch on a deadline (explicit fake-loader seam)", async () => {
323  vi.useFakeTimers();
324  const f = quickFixture();
325  let signal: AbortSignal | null | undefined;
326  let snapshot!: Snapshot;
327  f.ports.quick = {
328    evidence: "mocked-worker-loader", files: value => { snapshot = value; return {} as Fetcher; },
329    loader: { load: () => ({getEntrypoint: () => ({fetch: async (_url: unknown, init: RequestInit) => {
330      if (init.method !== "POST") return new Response("ready");
331      signal = init.signal;
332      return new Promise<Response>(() => {});
333    }})} as unknown as WorkerStub) },
334  };
335  try {
336    const pending = runReviewChecks(request, f.ports);
337    await vi.advanceTimersByTimeAsync(QUICK_TIMEOUT_MS + 1);
338    const record = await pending;
339    expect(signal?.aborted).toBe(true);
340    await expect(snapshot.read("brand.json")).rejects.toThrow();
341    expect(record.receipt).toMatchObject({evidence:"mocked-worker-loader",checks:[{outcome:"timed-out"}]});
342    await expect(requirePassingReceipt(record, {...review,checks:[quick],digest:record.digest})).rejects.toThrow();
343  } finally { vi.useRealTimers(); }
344});
345
346it("lists literal file prefixes, executable files, and refuses mutable object references", async () => {
347  const f = quickFixture();
348  expect(await new Snapshot(f.repo, COMMIT).list("bra")).toEqual(["brand.json"]);
349  f.repo.readTree.mockResolvedValue([{name:"brand.json",type:"exec",mode:"100755",hash:FILE}]);
350  expect(await new Snapshot(f.repo, COMMIT).read("brand.json")).toBe("fixture");
351  f.repo.readTree.mockResolvedValue([{name:"brand.json",type:"blob",mode:"100644",hash:"main"}]);
352  await expect(new Snapshot(f.repo, COMMIT).read("brand.json")).rejects.toThrow();
353});
354