import test from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, existsSync, readdirSync, readFileSync, symlinkSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { makeCfExec, makeHouseCfExec, type RunFn, type FetchFn } from "../cf-exec.js"; const cred = { apiToken: "cfat_x", accountId: "acc-h" }; function recordingRun(stdout: string) { const calls: Array<{ cmd: string; args: string[]; env: Record; cwd?: string; }> = []; const run: RunFn = async (cmd, args, env, opts) => { calls.push({ cmd, args, env, cwd: opts?.cwd }); return { stdout }; }; return { run, calls }; } // r2List and d1Create hit the Cloudflare API, not wrangler, so they need a // fetch seam. A run that throws proves they never shell wrangler — in // particular that no `--json` reaches a `wrangler d1 create` invocation. const throwingRun: RunFn = async () => { throw new Error("must not shell wrangler"); }; function recordingFetch(response: { ok: boolean; status: number; body: string }) { const calls: Array<{ url: string; init: { method: string; headers: Record; body?: string | Uint8Array }; }> = []; const fetchFn: FetchFn = async (url, init) => { calls.push({ url, init }); return { ok: response.ok, status: response.status, text: async () => response.body, // Task 1691 — r2ObjectGet reads the raw object body, so the seam carries // arrayBuffer alongside text. The fake derives it from the same body. arrayBuffer: async () => new TextEncoder().encode(response.body).buffer as ArrayBuffer, }; }; return { fetchFn, calls }; } // r2List follows the R2 pagination cursor, so its tests need a fetch that // returns a different response per call. recordingFetch returns one fixed // response for every call and stays as-is for the single-request paths. function sequenceFetch(responses: Array<{ ok: boolean; status: number; body: string }>) { const calls: Array<{ url: string; init: { method: string; headers: Record; body?: string | Uint8Array }; }> = []; const fetchFn: FetchFn = async (url, init) => { calls.push({ url, init }); const r = responses[calls.length - 1]; if (!r) throw new Error(`unexpected fetch call #${calls.length} to ${url}`); return { ok: r.ok, status: r.status, text: async () => r.body, arrayBuffer: async () => new TextEncoder().encode(r.body).buffer as ArrayBuffer, }; }; return { fetchFn, calls }; } function bucketPage(names: string[], cursor?: string) { return JSON.stringify({ success: true, errors: [], messages: [], result: { buckets: names.map((name) => ({ name, creation_date: "2026-01-01T00:00:00Z" })) }, result_info: cursor === undefined ? {} : { cursor, per_page: 20 }, }); } test("d1List parses --json output and injects the house token", async () => { const { run, calls } = recordingRun(JSON.stringify([{ name: "gls-leads", uuid: "u1" }])); const cf = makeCfExec(cred, run); assert.deepEqual(await cf.d1List(), [{ name: "gls-leads", uuid: "u1" }]); assert.equal(calls[0].cmd, "npx"); assert.deepEqual(calls[0].args, ["wrangler", "d1", "list", "--json"]); assert.equal(calls[0].env.CLOUDFLARE_API_TOKEN, "cfat_x"); assert.equal(calls[0].env.CLOUDFLARE_ACCOUNT_ID, "acc-h"); }); test("d1Query runs execute --remote with the SQL", async () => { const { run, calls } = recordingRun(JSON.stringify([{ results: [] }])); const cf = makeCfExec(cred, run); await cf.d1Query("gls-leads", "SELECT 1"); assert.equal(calls[0].cmd, "npx"); assert.deepEqual(calls[0].args, [ "wrangler", "d1", "execute", "gls-leads", "--remote", "--json", "--command", "SELECT 1", ]); }); test("d1Create creates the database via the D1 API with the house token, never wrangler", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { uuid: "new-db", name: "gls-new", created_at: "2026-01-01T00:00:00Z" }, }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.d1Create("gls-new"), { uuid: "new-db" }); assert.equal(calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/d1/database"); assert.equal(calls[0].init.method, "POST"); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); assert.equal(calls[0].init.headers["Content-Type"], "application/json"); // init.body widened to string | Uint8Array for the object-put path (Task // 1691); d1Create still sends a JSON string. assert.deepEqual(JSON.parse(String(calls[0].init.body ?? "")), { name: "gls-new" }); }); test("d1Create throws with the API error body on a non-2xx response", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 403, body: JSON.stringify({ success: false, errors: [{ code: 10000, message: "Authentication error" }] }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.d1Create("gls-new"), /403[\s\S]*Authentication error/); }); test("d1Create throws on a 200 whose body is not a success envelope, retaining the body", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ unexpected: "shape" }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.d1Create("gls-new"), /unexpected/); }); test("r2Create shells wrangler r2 bucket create", async () => { const { run, calls } = recordingRun(""); const cf = makeCfExec(cred, run); await cf.r2Create("gls-assets"); assert.equal(calls[0].cmd, "npx"); assert.deepEqual(calls[0].args, ["wrangler", "r2", "bucket", "create", "gls-assets"]); }); test("r2List returns the account's buckets from the R2 API with the house token", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { buckets: [ { name: "gls-assets", creation_date: "2026-01-01T00:00:00Z" }, { name: "gls-docs", creation_date: "2026-01-02T00:00:00Z" }, ], }, }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2List(), [{ name: "gls-assets" }, { name: "gls-docs" }]); assert.equal(calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets"); assert.equal(calls[0].init.method, "GET"); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); }); test("r2List returns [] for an account with no buckets", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { buckets: [] } }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2List(), []); }); test("r2List throws with the API error body on a non-2xx response", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 403, body: JSON.stringify({ success: false, errors: [{ code: 10000, message: "Authentication error" }] }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), /403[\s\S]*Authentication error/); }); test("r2List throws on a 200 whose body is not a success envelope, retaining the body", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ unexpected: "shape" }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), /unexpected/); }); // `null` is valid JSON, so it survives parseJson and reaches the envelope // guard as a null parsed value. It must still surface its body like every other // malformed 2xx shape, never a bare TypeError with the body dropped. test("a 2xx body of JSON null throws with the body, not a TypeError", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: "null" }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), { message: "storage-broker: r2 bucket list API error: null", }); }); test("non-JSON output throws with the raw body", async () => { const { run } = recordingRun("not json"); const cf = makeCfExec(cred, run); await assert.rejects(() => cf.d1List(), /not json/); }); // Task 1665 — the R2 List-Buckets endpoint paginates by cursor. A single // unpaginated request silently truncates the bucket set on a large house // account, and the audit reads a truncated list as "those buckets are absent", // under-reporting strays. per_page is never sent: its default and cap are // unconfirmed, and omitting it makes the loop correct without guessing. test("r2List follows the pagination cursor and assembles every page", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets", "gls-docs"], "cur-1") }, { ok: true, status: 200, body: bucketPage(["acme-assets"]) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2List(), [ { name: "gls-assets" }, { name: "gls-docs" }, { name: "acme-assets" }, ]); assert.equal(calls.length, 2); // The first request carries no query string; only follow-ups carry the cursor. assert.equal(calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets"); assert.equal( calls[1].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets?cursor=cur-1", ); assert.equal(calls[1].init.headers.Authorization, "Bearer cfat_x"); }); test("r2List never sends per_page", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") }, { ok: true, status: 200, body: bucketPage(["gls-docs"]) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2List(); for (const c of calls) assert.equal(c.url.includes("per_page"), false); }); test("r2List stops on an empty-string cursor", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "") }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2List(), [{ name: "gls-assets" }]); assert.equal(calls.length, 1); }); test("r2List percent-encodes the cursor", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "a b+c/d=") }, { ok: true, status: 200, body: bucketPage([]) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2List(); assert.equal( calls[1].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets?cursor=a%20b%2Bc%2Fd%3D", ); }); test("r2List stops on a null cursor", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { buckets: [{ name: "gls-assets" }] }, result_info: { cursor: null }, }), }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2List(), [{ name: "gls-assets" }]); assert.equal(calls.length, 1); }); test("r2List throws on a repeated cursor instead of looping forever", async () => { const { fetchFn } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") }, { ok: true, status: 200, body: bucketPage(["gls-docs"], "cur-1") }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), /repeated pagination cursor: cur-1/); }); // A cursor cycle of period >= 2 never repeats the immediately-previous cursor, // so it would spin forever — re-appending the same buckets each pass — under a // guard that only compares against the last cursor seen. test("r2List throws on a cursor cycle that does not repeat consecutively", async () => { const { fetchFn } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-a") }, { ok: true, status: 200, body: bucketPage(["gls-docs"], "cur-b") }, { ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-a") }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), /repeated pagination cursor: cur-a/); }); test("r2List surfaces a mid-loop failure with its body, never a partial list", async () => { const { fetchFn } = sequenceFetch([ { ok: true, status: 200, body: bucketPage(["gls-assets"], "cur-1") }, { ok: false, status: 403, body: JSON.stringify({ success: false, errors: [{ code: 10000, message: "Authentication error" }], }), }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2List(), /403[\s\S]*Authentication error/); }); // Task 1670 — makeHouseCfExec resolves a scoped storage token from the house // minter via cf-token.sh and uses THAT token (never the minter) for wrangler. test("makeHouseCfExec runs wrangler with the scoped token, not the minter", async () => { const platformRoot = mkdtempSync(join(tmpdir(), "house-cfexec-")); const cfg = join(platformRoot, "config"); mkdirSync(cfg, { recursive: true }); const houseEnv = join(cfg, "cloudflare-house.env"); writeFileSync(houseEnv, "CLOUDFLARE_API_TOKEN=cfat_minter\nCLOUDFLARE_ACCOUNT_ID=acc-h\n"); let sawToken = ""; const run: RunFn = async (_cmd, args, env) => { if (args.some((a) => a.endsWith("cf-token.sh"))) { appendFileSync(houseEnv, "CF_STORAGE_TOKEN=scoped_tok\n"); return { stdout: "CF_STORAGE_TOKEN\n" }; } sawToken = env.CLOUDFLARE_API_TOKEN; return { stdout: "[]" }; }; const cf = await makeHouseCfExec(platformRoot, run); await cf.d1List(); assert.equal(sawToken, "scoped_tok", "wrangler saw the scoped token, not the minter"); }); // --------------------------------------------------------------------------- // Task 1691 — R2 object operations. Same discipline as r2List/d1Create: the // Cloudflare API behind an injectable fetch, wrangler never shelled (throwingRun // proves it), and every failure keeps its body. // --------------------------------------------------------------------------- function objectPage( keys: Array, opts: { cursor?: string; truncated?: boolean } = {}, ) { return JSON.stringify({ success: true, errors: [], messages: [], result: keys.map((k) => { const entry = typeof k === "string" ? { key: k, size: 10 } : k; return { key: entry.key, size: entry.size, etag: "e1", last_modified: "2026-01-01T00:00:00Z", }; }), result_info: { cursor: opts.cursor ?? "", is_truncated: opts.truncated ?? false }, }); } test("r2ObjectList returns a single page and never shells wrangler", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage(["a.jpg"]) }); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.r2ObjectList("acc-a-portal"), [ { key: "a.jpg", size: 10, etag: "e1", lastModified: "2026-01-01T00:00:00Z" }, ]); assert.equal(calls.length, 1); assert.equal( calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/r2/buckets/acc-a-portal/objects", ); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); }); test("r2ObjectList never sends per_page", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2ObjectList("b"); assert.equal(calls[0].url.includes("per_page"), false); }); test("r2ObjectList passes prefix through, encoded", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2ObjectList("b", "person a/"); assert.ok(calls[0].url.endsWith("/objects?prefix=person+a%2F"), calls[0].url); }); test("r2ObjectList follows the cursor to exhaustion", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: true }) }, { ok: true, status: 200, body: objectPage(["b.jpg"]) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]); assert.equal(calls.length, 2); assert.ok(calls[1].url.includes("cursor=c1")); }); // is_truncated is the authoritative continuation signal, not the presence of a // cursor: a last page may still carry one. test("r2ObjectList stops when is_truncated is false even if a cursor is present", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["a.jpg"], { cursor: "c1", truncated: false }) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2ObjectList("b"); assert.equal(calls.length, 1); }); // The response shape is doc-confirmed but never measured against a live account. // If it is not what we understand, that must be loud: a partial list is exactly // the bug this loop exists to prevent. test("r2ObjectList throws when truncated but no cursor is given, never a partial list", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: objectPage(["a.jpg"], { truncated: true }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectList("b"), /truncated but gave no pagination cursor/); }); test("r2ObjectList throws on a repeated cursor", async () => { const { fetchFn } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["a"], { cursor: "c1", truncated: true }) }, { ok: true, status: 200, body: objectPage(["b"], { cursor: "c1", truncated: true }) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectList("b"), /repeated pagination cursor/); }); test("r2ObjectList retains the body on a non-2xx", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectList("b"), /10000/); }); test("r2ObjectGet returns exact bytes and encodes the key segment-wise", async () => { // Binary that is not valid UTF-8: proves the body never round-trips through // text(), which would corrupt it. const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff]); const calls: string[] = []; const fetchFn: FetchFn = async (url) => { calls.push(url); return { ok: true, status: 200, text: async () => "unused", arrayBuffer: async () => bytes.buffer.slice(0) as ArrayBuffer, }; }; const cf = makeCfExec(cred, throwingRun, fetchFn); const got = await cf.r2ObjectGet("b", "person a/photo 1.jpg"); assert.deepEqual(Array.from(got), Array.from(bytes)); // Separators stay separators; spaces are escaped. assert.ok(calls[0].endsWith("/objects/person%20a/photo%201.jpg"), calls[0]); }); test("r2ObjectGet retains the body on a non-2xx", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 404, body: "no such key" }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectGet("b", "k"), /no such key/); }); test("r2ObjectPut sends the bytes as the request body", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); const bytes = new Uint8Array([1, 2, 3]); await cf.r2ObjectPut("b", "k.bin", bytes); assert.equal(calls[0].init.method, "PUT"); assert.deepEqual(calls[0].init.body, bytes); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); }); test("r2ObjectPut retains the body on a non-success envelope", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: '{"success":false,"errors":[{"code":10001}]}', }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectPut("b", "k", new Uint8Array([1])), /10001/); }); test("r2ObjectDelete issues a DELETE", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2ObjectDelete("b", "k"); assert.equal(calls[0].init.method, "DELETE"); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); }); test("r2ObjectDelete retains the body on a non-2xx", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 500, body: "boom" }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectDelete("b", "k"), /boom/); }); // Code review — is_truncated is itself an unmeasured fact (no live credential // exists to confirm the envelope). Driving the loop solely on it means a // response that omits it while still paginating truncates SILENTLY, which is // the exact failure this loop exists to prevent, and it would also make // r2ObjectList disagree with r2List directly above it, which drives the same // envelope on cursor presence alone. test("r2ObjectList follows a cursor when is_truncated is absent, rather than truncating silently", async () => { const pageWithoutTruncatedFlag = (keys: string[], cursor: string | null) => JSON.stringify({ success: true, errors: [], messages: [], result: keys.map((key) => ({ key, size: 1, etag: "e", last_modified: "t" })), result_info: cursor === null ? {} : { cursor }, }); const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: pageWithoutTruncatedFlag(["a.jpg"], "c1") }, { ok: true, status: 200, body: pageWithoutTruncatedFlag(["b.jpg"], null) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual((await cf.r2ObjectList("b")).map((o) => o.key), ["a.jpg", "b.jpg"]); assert.equal(calls.length, 2); }); // --- r2ObjectFind (Task 1697) --------------------------------------------- // // Sizing a get needs one record, not the prefix. These tests pin the two // properties that make the early exit safe: it matches the exact key, and it // stops at the page carrying it. test("r2ObjectFind returns the exact key when a prefix neighbour is listed first", async () => { // Not hypothetical. Measured 2026-07-16 against the live v4 API: prefix= // a/photo.jpg really does return a/photo.jpg.bak BEFORE a/photo.jpg, because // the listing is not in ascending UTF-8 binary order. Taking the first element // would size the neighbour, which either rejects a legal get or admits an // oversized one. const { fetchFn } = recordingFetch({ ok: true, status: 200, body: objectPage([ { key: "a/photo.jpg.bak", size: 999 }, { key: "a/photo.jpg", size: 3 }, ]), }); const cf = makeCfExec(cred, throwingRun, fetchFn); const found = await cf.r2ObjectFind("b", "a/photo.jpg"); assert.equal(found?.key, "a/photo.jpg"); assert.equal(found?.size, 3); }); test("r2ObjectFind sends the key as the prefix, encoded", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: objectPage([]) }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.r2ObjectFind("b", "person a/x.jpg"); assert.ok(calls[0].url.endsWith("/objects?prefix=person+a%2Fx.jpg"), calls[0].url); }); test("r2ObjectFind stops at the page carrying the match and fetches no further page", async () => { // The page says truncated with a cursor, so r2ObjectList would fetch page 2. // sequenceFetch throws on an unexpected call, so a second fetch fails here. const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: objectPage([{ key: "k", size: 7 }], { cursor: "c1", truncated: true }), }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); const found = await cf.r2ObjectFind("b", "k"); assert.equal(found?.size, 7); assert.equal(calls.length, 1); }); test("r2ObjectFind finds a match on a later page, never assuming position", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["x.jpg"], { cursor: "c1", truncated: true }) }, { ok: true, status: 200, body: objectPage([{ key: "k", size: 5 }], { truncated: false }) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); const found = await cf.r2ObjectFind("b", "k"); assert.equal(found?.size, 5); assert.equal(calls.length, 2); }); test("r2ObjectFind returns null when the key is absent after exhausting pages", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["k.bak"], { cursor: "c1", truncated: true }) }, { ok: true, status: 200, body: objectPage(["k.zzz"], { truncated: false }) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.equal(await cf.r2ObjectFind("b", "k"), null); assert.equal(calls.length, 2); }); test("r2ObjectFind throws on a repeated cursor instead of looping forever", async () => { const { fetchFn } = sequenceFetch([ { ok: true, status: 200, body: objectPage(["x"], { cursor: "c1", truncated: true }) }, { ok: true, status: 200, body: objectPage(["y"], { cursor: "c1", truncated: true }) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectFind("b", "k"), /repeated pagination cursor/); }); test("r2ObjectFind throws when truncated but no cursor is given", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: objectPage(["x"], { cursor: "", truncated: true }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectFind("b", "k"), /truncated but gave no pagination cursor/); }); test("r2ObjectFind retains the body on a non-2xx", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 403, body: '{"errors":[{"code":10000}]}' }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.r2ObjectFind("b", "k"), /10000/); }); // Code review — the early exit changes observable behaviour on an anomalous // page, and an unpinned behaviour change is how a regression starts. Pinned as a // decision rather than left implicit. // // A page that reports truncated with no cursor is an anomaly r2ObjectList throws // on, because a partial listing would corrupt the audit. r2ObjectFind returns at // the match and never reaches that check, so the same response yields the size // instead of an error. That is correct: the size is Cloudflare's own record for // the exact key, the cap is enforced on it, and the anomaly concerns pages the // sizing does not need. The audit's behaviour on the same response is unchanged // and is pinned by "r2ObjectList throws when truncated but no cursor is given". test("r2ObjectFind returns a match found before an anomalous page's guard is reached", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: objectPage([{ key: "k", size: 4 }], { cursor: "", truncated: true }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); const found = await cf.r2ObjectFind("b", "k"); assert.equal(found?.size, 4); }); // --- Task 1728: house-run Pages publishing ------------------------------- function projectPage(names: string[], cursor?: string) { return JSON.stringify({ success: true, errors: [], messages: [], result: names.map((name) => ({ name, subdomain: `${name}.pages.dev` })), result_info: cursor === undefined ? {} : { cursor, per_page: 20 }, }); } // A real site tree on disk. pagesDeploy copies the tree before uploading it, so // these tests cannot hand it a path that does not exist. function siteTree(files: Record): string { const dir = mkdtempSync(join(tmpdir(), "pages-site-")); for (const [rel, body] of Object.entries(files)) { const abs = join(dir, rel); mkdirSync(dirname(abs), { recursive: true }); writeFileSync(abs, body); } return dir; } const PORTAL = { "index.html": "", "portal.js": "export {}", "functions/api/files.js": "export {}", "wrangler.toml": 'name = "kit-data"\ndatabase_id = "secret-looking"\n', }; /** Snapshots the uploaded directory's contents at call time, because * pagesDeploy deletes the staging tree before it returns. */ function snapshottingRun(stdout: string) { const calls: Array<{ args: string[]; cwd?: string; uploadDir: string; uploaded: string[]; }> = []; const run: RunFn = async (_cmd, args, _env, opts) => { const uploadDir = args[3]; const uploaded: string[] = []; const walk = (d: string, prefix: string) => { for (const entry of readdirSync(d, { withFileTypes: true })) { if (entry.isDirectory()) walk(join(d, entry.name), `${prefix}${entry.name}/`); else uploaded.push(`${prefix}${entry.name}`); } }; walk(uploadDir, ""); calls.push({ args, cwd: opts?.cwd, uploadDir, uploaded: uploaded.sort() }); return { stdout }; }; return { run, calls }; } const DEPLOYED = "✨ Deployment complete! Take a peek over at https://abc123.glsmithandsons.pages.dev\n"; test("pagesDeploy shells npx wrangler (never bare) and returns the deployment url", async () => { const dir = siteTree(PORTAL); const { run, calls } = recordingRun(`Uploading... (2/2)\n${DEPLOYED}`); const cf = makeCfExec(cred, run); const { url } = await cf.pagesDeploy(dir, "glsmithandsons", "main"); assert.equal(url, "https://abc123.glsmithandsons.pages.dev"); assert.equal(calls[0].cmd, "npx"); assert.deepEqual(calls[0].args.slice(0, 3), ["wrangler", "pages", "deploy"]); assert.deepEqual(calls[0].args.slice(4), [ "--project-name", "glsmithandsons", "--branch", "main", ]); assert.equal(calls[0].env.CLOUDFLARE_API_TOKEN, "cfat_x"); }); test("pagesDeploy omits --branch when no branch is given", async () => { const dir = siteTree(PORTAL); const { run, calls } = recordingRun(DEPLOYED); const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "glsmithandsons"); assert.deepEqual(calls[0].args.slice(4), ["--project-name", "glsmithandsons"]); }); test("pagesDeploy throws with the body when wrangler prints no deployment url", async () => { const dir = siteTree(PORTAL); const { run } = recordingRun("Uploading... (0/0)\nsomething went sideways\n"); const cf = makeCfExec(cred, run); await assert.rejects( () => cf.pagesDeploy(dir, "glsmithandsons"), /something went sideways/, "a url-less wrangler run must surface its output, never report a silent success", ); }); // --- Task 1782: the uploaded set is the broker's, not whatever the tree holds --- test("pagesDeploy uploads a staged copy with the wrangler config left out", async () => { const dir = siteTree(PORTAL); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { excluded } = await cf.pagesDeploy(dir, "kit-data", "main"); assert.notEqual( calls[0].uploadDir, dir, "the directory handed to wrangler must not be the tree that holds the config", ); assert.deepEqual( calls[0].uploaded, ["functions/api/files.js", "index.html", "portal.js"], "every site file is published and the config is not", ); assert.deepEqual(excluded, ["wrangler.toml"]); }); test("pagesDeploy still runs wrangler in the tree that holds the config", async () => { const dir = siteTree(PORTAL); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "kit-data", "main"); // wrangler finds its config by walking up from the working directory, so the // cwd is what keeps the D1 and R2 bindings applying once the file itself has // left the upload. It is also still the writable scratch anchor Task 1770 // pinned it for. assert.equal(calls[0].cwd, dir); }); test("pagesDeploy excludes every config filename wrangler itself discovers", async () => { const dir = siteTree({ "index.html": "", "wrangler.toml": "x", "wrangler.json": "{}", "wrangler.jsonc": "{}", }); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { excluded } = await cf.pagesDeploy(dir, "kit-data"); assert.deepEqual(calls[0].uploaded, ["index.html"]); assert.deepEqual(excluded, ["wrangler.json", "wrangler.jsonc", "wrangler.toml"]); }); test("pagesDeploy keeps a nested file that merely shares the config's name", async () => { const dir = siteTree({ "index.html": "", "wrangler.toml": "x", "docs/wrangler.toml": "an example the site publishes on purpose", }); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { excluded } = await cf.pagesDeploy(dir, "kit-data"); assert.deepEqual(calls[0].uploaded, ["docs/wrangler.toml", "index.html"]); assert.deepEqual(excluded, ["wrangler.toml"], "only the config wrangler reads is dropped"); }); test("pagesDeploy reports nothing excluded for a tree that carries no config", async () => { const dir = siteTree({ "index.html": "" }); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { excluded } = await cf.pagesDeploy(dir, "glsmithandsons"); assert.deepEqual(calls[0].uploaded, ["index.html"]); assert.deepEqual(excluded, []); }); test("pagesDeploy publishes what a symlink pointed at, including one leaving the tree", async () => { // wrangler's own upload walk stats each entry, so deploying in place published // the link's target. Copying links verbatim would strand an escaping one. // A relative link escaping the tree is the shape that would dangle if the copy // reproduced it verbatim somewhere else on disk. It does not: node's fs.cp // rewrites every symlink to its resolved absolute target, so the staged link // still points at the real file and wrangler's stat-following walk publishes // it exactly as an in-place deploy did. This pins that behaviour rather than // trusting it — the whole change rests on the staged tree being what wrangler // would have uploaded. const base = mkdtempSync(join(tmpdir(), "pages-base-")); mkdirSync(join(base, "shared")); writeFileSync(join(base, "shared", "shared.css"), "body{}"); const dir = join(base, "site"); mkdirSync(dir); writeFileSync(join(dir, "index.html"), ""); writeFileSync(join(dir, "wrangler.toml"), "x"); symlinkSync(join("..", "shared", "shared.css"), join(dir, "theme.css")); // Read during the run: the staging tree is gone by the time pagesDeploy returns. let staged = ""; const run: RunFn = async (_cmd, args) => { staged = readFileSync(join(args[3], "theme.css"), "utf8"); return { stdout: DEPLOYED }; }; const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "kit-data"); assert.equal( staged, "body{}", "the staged copy carries the target's bytes, not a link that resolves to nothing", ); }); test("pagesDeploy removes the staging tree on the success path", async () => { const dir = siteTree(PORTAL); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "kit-data"); assert.equal(existsSync(calls[0].uploadDir), false); assert.equal(existsSync(join(dir, "wrangler.toml")), true, "the real tree is untouched"); }); test("pagesDeploy removes the staging tree when wrangler fails", async () => { const dir = siteTree(PORTAL); let uploadDir = ""; const run: RunFn = async (_cmd, args) => { uploadDir = args[3]; throw new Error("wrangler exploded"); }; const cf = makeCfExec(cred, run); await assert.rejects(() => cf.pagesDeploy(dir, "kit-data"), /wrangler exploded/); assert.notEqual(uploadDir, ""); assert.equal(existsSync(uploadDir), false, "a failed deploy must not strand a copy of the site"); }); test("pagesProjectList follows the pagination cursor to exhaustion via the API, never wrangler", async () => { const { fetchFn, calls } = sequenceFetch([ { ok: true, status: 200, body: projectPage(["glsmithandsons"], "c1") }, { ok: true, status: 200, body: projectPage(["ac-electrical"]) }, ]); const cf = makeCfExec(cred, throwingRun, fetchFn); assert.deepEqual(await cf.pagesProjectList(), [ { name: "glsmithandsons" }, { name: "ac-electrical" }, ]); assert.equal(calls.length, 2); assert.ok(!calls[0].url.includes("per_page"), "per_page is never sent (its max is unconfirmed)"); assert.ok(calls[1].url.includes("cursor=c1"), "the second request carries the cursor"); }); // --- Task 1766: the deploy creates the project it publishes to ------------- test("pagesProjectCreate creates the project via the Pages API with the house token, never wrangler", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { name: "kit-data", subdomain: "kit-data.pages.dev", production_branch: "main" }, }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.pagesProjectCreate("kit-data", "main"); assert.equal(calls[0].url, "https://api.cloudflare.com/client/v4/accounts/acc-h/pages/projects"); assert.equal(calls[0].init.method, "POST"); assert.equal(calls[0].init.headers.Authorization, "Bearer cfat_x"); assert.equal(calls[0].init.headers["Content-Type"], "application/json"); assert.deepEqual(JSON.parse(String(calls[0].init.body ?? "")), { name: "kit-data", production_branch: "main", }); }); test("pagesProjectCreate carries the caller's branch as the production branch", async () => { const { fetchFn, calls } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: { name: "kit-data" } }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await cf.pagesProjectCreate("kit-data", "release"); assert.equal( JSON.parse(String(calls[0].init.body ?? "")).production_branch, "release", "the production branch is the caller's, never hard-coded", ); }); test("pagesProjectCreate throws with the API error body on a non-2xx response", async () => { const { fetchFn } = recordingFetch({ ok: false, status: 403, body: JSON.stringify({ success: false, errors: [{ code: 10000, message: "Authentication error" }] }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects(() => cf.pagesProjectCreate("kit-data", "main"), /403[\s\S]*Authentication error/); }); test("pagesProjectCreate throws on a 200 whose envelope carries no project name, retaining the body", async () => { const { fetchFn } = recordingFetch({ ok: true, status: 200, body: JSON.stringify({ success: true, errors: [], messages: [], result: {} }), }); const cf = makeCfExec(cred, throwingRun, fetchFn); await assert.rejects( () => cf.pagesProjectCreate("kit-data", "main"), /pages project create/, "a name-less 2xx is an anomaly to surface with its body, never a silent create", ); }); // --- Task 1770: wrangler runs in the deploy dir, not wherever it infers ----- test("pagesDeploy pins wrangler's working directory to the deploy dir", async () => { const { run, calls } = recordingRun( "\u2728 Deployment complete! Take a peek over at https://abc.kit-data.pages.dev\n", ); const dir = siteTree(PORTAL); const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "kit-data", "main"); // Unpinned, wrangler resolves .wrangler/tmp against a directory it infers -- // on a real install that is the immutable server tree, where the mkdir is // EPERM and every deploy dies before uploading. assert.equal( calls[0].cwd, dir, "wrangler must run in the deploy dir so its scratch dir lands beside the tree it uploads", ); }); // --- Task 1917: the asset root is the declared output dir, not its parent --- const SIGN_TREE = { "wrangler.toml": 'name = "gls-sign"\npages_build_output_dir = "public"\n', "public/index.html": "", "public/quote-patel.pdf": "%PDF-", "functions/api/sign.js": "export {}", }; test("pagesDeploy uploads the declared output dir, so its contents sit at the served root", async () => { const dir = siteTree(SIGN_TREE); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { uploadDir, uploadDirSource } = await cf.pagesDeploy(dir, "gls-sign-1848", "main"); assert.deepEqual( calls[0].uploaded, ["index.html", "quote-patel.pdf"], "the declared output dir's contents are the served root, not a /public/ subtree", ); assert.equal(uploadDir, join(dir, "public")); assert.equal(uploadDirSource, "wrangler.toml"); }); test("pagesDeploy still runs wrangler in the site root when the output dir is declared", async () => { const dir = siteTree(SIGN_TREE); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); await cf.pagesDeploy(dir, "gls-sign-1848", "main"); assert.equal( calls[0].cwd, dir, "cwd carries the config's bindings and wrangler's functions/ lookup; it is the site root either way", ); }); test("pagesDeploy reports the site root as the source when nothing is declared", async () => { const dir = siteTree(PORTAL); const { run } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { uploadDir, uploadDirSource } = await cf.pagesDeploy(dir, "kit-data", "main"); assert.equal(uploadDir, dir); assert.equal(uploadDirSource, "site-root"); }); test("pagesDeploy drops a wrangler config sitting inside the declared output dir", async () => { const dir = siteTree({ ...SIGN_TREE, "public/wrangler.toml": 'database_id = "secret-looking"\n' }); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); const { excluded } = await cf.pagesDeploy(dir, "gls-sign-1848", "main"); assert.deepEqual(calls[0].uploaded, ["index.html", "quote-patel.pdf"]); assert.deepEqual(excluded, ["wrangler.toml"]); }); test("pagesDeploy refuses to deploy when the declared output dir is absent", async () => { const dir = siteTree({ "wrangler.toml": 'pages_build_output_dir = "public"\n' }); const { run, calls } = snapshottingRun(DEPLOYED); const cf = makeCfExec(cred, run); await assert.rejects( () => cf.pagesDeploy(dir, "gls-sign-1848", "main"), new RegExp(join(dir, "public")), "a declared-but-absent output dir names the path; falling back to the site root is the defect", ); assert.equal(calls.length, 0, "no wrangler run may happen once resolution has failed"); });