import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import { setTimeout } from "node:timers/promises"; import dedent from "ts-dedent"; import { beforeAll, describe, it } from "vitest"; import { CLOUDFLARE_ACCOUNT_ID } from "../helpers/account-id"; import { WranglerE2ETestHelper } from "../helpers/e2e-wrangler-test"; import { fetchText } from "../helpers/fetch-text"; import { normalizeOutput } from "../helpers/normalize"; import { makeRoot, seed } from "../helpers/setup"; import { waitFor, waitForLong } from "../helpers/wait-for"; describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( "wrangler dev - remote bindings", () => { const remoteWorkerName = "preserve-e2e-wrangler-remote-worker"; const alternativeRemoteWorkerName = "preserve-e2e-wrangler-remote-worker-alt"; const helper = new WranglerE2ETestHelper(); beforeAll(async () => { await helper.seed(resolve(__dirname, "./workers")); await Promise.all([ helper.ensureWorkerDeployed({ entryPoint: "remote-worker.js", workerName: remoteWorkerName, }), helper.ensureWorkerDeployed({ entryPoint: "alt-remote-worker.js", workerName: alternativeRemoteWorkerName, }), ]); }, 35_000); it("handles both remote and local service bindings at the same time", async ({ expect, }) => { await spawnLocalWorker(helper); await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "local-and-remote-service-bindings.js", compatibility_date: "2025-05-07", services: [ { binding: "LOCAL_WORKER", service: "local-worker", remote: false }, { binding: "REMOTE_WORKER", service: remoteWorkerName, remote: true, }, ], }), }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toMatchInlineSnapshot(` "LOCAL: Hello from a local worker! REMOTE: Hello from a remote worker " `); }); it("allows code changes during development", async ({ expect }) => { await spawnLocalWorker(helper); await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "simple-service-binding.js", compatibility_date: "2025-05-07", services: [ { binding: "REMOTE_WORKER", service: remoteWorkerName, remote: true, }, ], }), }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( `"REMOTE: Hello from a remote worker"` ); const indexContent = await readFile( `${helper.tmpPath}/simple-service-binding.js`, "utf8" ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent.replace( "REMOTE:", "The remote worker responded with:" ), "utf8" ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( `"The remote worker responded with: Hello from a remote worker"` ); await writeFile( `${helper.tmpPath}/simple-service-binding.js`, indexContent, "utf8" ); await setTimeout(500); await expect(fetchText(url)).resolves.toMatchInlineSnapshot( `"REMOTE: Hello from a remote worker"` ); }); it("handles workers AI alongside a local service binding", async ({ expect, }) => { await spawnLocalWorker(helper); await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "local-service-binding-and-remote-ai.js", compatibility_date: "2025-05-07", ai: { binding: "AI", }, services: [ { binding: "LOCAL_WORKER", service: "local-worker", remote: false }, ], }), }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toMatchInlineSnapshot(` "LOCAL: Hello from a local worker! REMOTE: This is a response from Workers AI. " `); }); it("should only include logs from the user Wrangler session (i.e. a single list of attached bindings, and only one ready message)", async ({ expect, }) => { await spawnLocalWorker(helper); await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "ai.js", compatibility_date: "2025-05-07", ai: { binding: "AI", }, }), }); const worker = helper.runLongLived("wrangler dev"); const { url } = await worker.waitForReady(); await expect(fetchText(url)).resolves.toContain( "This is a response from Workers AI." ); // This should only include logs from the user Wrangler session (i.e. a single list of attached bindings, and only one ready message) await waitFor(() => expect(normalizeOutput(worker.currentOutput)).toEqual(dedent` Your Worker has access to the following bindings: Binding Resource Mode env.AI AI remote ⎔ Establishing remote connection... ▲ [WARNING] AI bindings always access remote resources, and so may incur usage charges even in local dev. To suppress this warning, set \`remote: true\` for the binding definition in your configuration file. ⎔ Starting local server... [wrangler:info] Ready on http://: [wrangler:info] GET / 200 OK (TIMINGS) `) ); }); describe("shows helpful error logs", () => { it("when a remote service binding is not properly configured", async ({ expect, }) => { await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "simple-service-binding.js", compatibility_date: "2025-05-07", services: [ { binding: "REMOTE_WORKER", service: "non-existent-service-binding", remote: true, }, ], }), }); const worker = helper.runLongLived("wrangler dev"); // The error appears only after the remote proxy session validates // the binding against the Cloudflare API, so use the longer timeout. await waitForLong(() => expect(worker.currentOutput).toContain( "Service binding 'REMOTE_WORKER' references Worker 'non-existent-service-binding' which was not found." ) ); }); it("when a remote KV binding is not properly configured", async ({ expect, }) => { await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-test", main: "kv.js", compatibility_date: "2025-05-07", kv_namespaces: [ { binding: "KV_BINDING", id: "non-existent-kv", remote: true, }, ], }), }); const worker = helper.runLongLived("wrangler dev"); // The error appears only after the remote proxy session validates // the binding against the Cloudflare API, so use the longer timeout. await waitForLong(() => expect(worker.currentOutput).toContain( "KV namespace 'non-existent-kv' is not valid." ) ); }); }); describe("multi-worker", () => { it("handles both remote and local service bindings at the same time in all workers", async ({ expect, }) => { await helper.seed({ "wrangler.json": JSON.stringify({ name: "remote-bindings-mixed-bindings-multi-worker-test", main: "local-and-remote-service-bindings.js", compatibility_date: "2025-05-07", services: [ { binding: "LOCAL_WORKER", service: "local-test-worker", remote: false, }, { binding: "REMOTE_WORKER", service: remoteWorkerName, remote: true, }, ], }), }); const localTest = makeRoot(); await seed(localTest, { "wrangler.json": JSON.stringify({ name: "local-test-worker", main: "index.js", compatibility_date: "2025-05-07", services: [ { // Note: we use the same binding name but bound to a difference service binding: "REMOTE_WORKER", service: alternativeRemoteWorkerName, remote: true, }, ], }), "index.js": dedent` export default { async fetch(request, env) { const remoteWorkerText = await (await env.REMOTE_WORKER.fetch(request)).text(); return new Response(\`[local-test-worker]REMOTE: \${remoteWorkerText}\`); } }`, }); const worker = helper.runLongLived( `wrangler dev -c wrangler.json -c ${localTest}/wrangler.json` ); // Multi-worker with remote bindings on both workers requires two // serialised remote proxy sessions (API calls) before "Ready on" // can appear, so give it more time than the default 15s. const { url } = await worker.waitForReady(30_000); await expect(fetchText(url)).resolves.toMatchInlineSnapshot(` "LOCAL: [local-test-worker]REMOTE: Hello from an alternative remote worker REMOTE: Hello from a remote worker " `); }); }); } ); async function spawnLocalWorker(helper: WranglerE2ETestHelper): Promise { const local = makeRoot(); await seed(local, { "wrangler.json": JSON.stringify({ name: "local-worker", main: "index.js", compatibility_date: "2025-05-07", }), "index.js": dedent` export default { fetch(request) { return new Response("Hello from a local worker!"); } }`, }); const localWorker = helper.runLongLived("wrangler dev", { cwd: local }); await localWorker.waitForReady(); }