Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-runner/__tests__/quick.test.ts
Snapshot at 34e3175
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