import { getWorkerRegistry, Miniflare } from "miniflare"; import { describe, onTestFinished, test, vi } from "vitest"; import { useDispose, useTmp } from "./test-shared"; import type { MiniflareOptions, WorkerRegistry } from "miniflare"; describe.sequential("DevRegistry", () => { test("fetch to service worker", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], script: `addEventListener("fetch", (event) => { event.respondWith(new Response("Hello from service worker!")); })`, }); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { return await env.SERVICE.fetch(request); } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Hello from service worker!"); expect(res.status).toBe(200); // Kill the remote worker to see if it fails gracefully await remote.dispose(); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); }, { timeout: 10_000, interval: 100 } ); }); test("fetch to module worker", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const response = await env.SERVICE.fetch(request.url); const text = await response.text(); return new Response("Response from remote worker: " + text, { status: response.status, }); } } `, }); useDispose(local); const res = await local.dispatchFetch("http://example.com?name=World"); expect(await res.text()).toBe( `Response from remote worker: Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); const name = url.searchParams.get("name") ?? 'anonymous'; return new Response("Hello " + name); } } `, }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://example.com?name=World"); const result = await res.text(); expect(result).toBe("Response from remote worker: Hello World"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("WebSocket upgrade to module worker", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const wsResponse = await env.SERVICE.fetch(request.url, { headers: { Upgrade: "websocket" } }); if (wsResponse.webSocket) { wsResponse.webSocket.accept(); const messagePromise = new Promise((resolve) => { wsResponse.webSocket.addEventListener("message", (event) => { resolve(event.data); }); }); // Test bidirectional communication wsResponse.webSocket.send("ping"); const response = await messagePromise; return new Response(\`WebSocket communication successful: \${response}\`, { status: 200, }); } return new Response("WebSocket upgrade failed", { status: 500, }); } } `, }); useDispose(local); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { // Handle WebSocket upgrade requests if (request.headers.get("Upgrade") === "websocket") { const [server, client] = Object.values(new WebSocketPair()); server.accept(); server.addEventListener("message", (event) => { // Echo back with a response to test bidirectional communication if (event.data === "ping") { server.send("pong"); } }); return new Response(null, { status: 101, webSocket: client }); } // This test only focuses on WebSocket, no HTTP handling needed return new Response("Not a WebSocket request", { status: 400 }); } } `, }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://example.com"); const result = await res.text(); expect(result).toBe("WebSocket communication successful: pong"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("RPC to default entrypoint", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const result = await env.SERVICE.ping(); return new Response("Response from remote worker: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); const result = await res.text(); expect(result).toBe("Response from remote worker: pong"); }, { timeout: 10_000, interval: 100 } ); // Kill the remote worker to see if it fails gracefully await remote.dispose(); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); }, { timeout: 10_000, interval: 100 } ); }); test("RPC to custom entrypoint", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", entrypoint: "TestEntrypoint", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const result = await env.SERVICE.ping(); return new Response("Response from remote worker: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); const result = await res.text(); expect(result).toBe("Response from remote worker: pong"); }, { timeout: 10_000, interval: 100 } ); await remote.dispose(); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); }, { timeout: 10_000, interval: 100 } ); }); test("service binding props are propagated across instances", async ({ expect, }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class PropsEntrypoint extends WorkerEntrypoint { getProps() { return this.ctx.props; } } `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", entrypoint: "PropsEntrypoint", props: { foo: 123, bar: { baz: "hello from props" } }, }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env) { return Response.json(await env.SERVICE.getProps()); } } `, }); useDispose(local); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); const json = await res.json(); expect(res.status).toBe(200); expect(json).toEqual({ foo: 123, bar: { baz: "hello from props" }, }); }, { timeout: 10_000, interval: 100 } ); }); test("fetch to module worker with node bindings", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { return new Response("Not implemented", { status: 501 }); } } `, }); useDispose(local); const bindings = await local.getBindings>(); await vi.waitFor( async () => { const res = await bindings.SERVICE.fetch( "http://example.com?name=World" ); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); }, { timeout: 10_000, interval: 100 } ); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); const name = url.searchParams.get("name") ?? 'anonymous'; return new Response("Hello " + name); } } `, }); await remote.ready; await vi.waitFor( async () => { const res = await bindings.SERVICE.fetch( "http://example.com?name=World" ); const result = await res.text(); expect(result).toBe("Hello World"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); await remote.dispose(); await vi.waitFor( async () => { const res = await bindings.SERVICE.fetch( "http://example.com?name=World" ); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); }, { timeout: 10_000, interval: 100 } ); }); test("RPC to default entrypoint with node bindings", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { return new Response("Not implemented", { status: 501 }); } } `, }); useDispose(local); const env = await local.getBindings>(); await vi.waitFor( async () => { try { const result = await env.SERVICE.ping(); throw new Error(`Expected error, got result: ${result}`); } catch (e) { expect(e instanceof Error ? e.message : `${e}`).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); } }, { timeout: 10_000, interval: 100 } ); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }); await remote.ready; await vi.waitFor( async () => { const result = await env.SERVICE.ping(); expect(result).toBe("pong"); }, { timeout: 10_000, interval: 100 } ); // Kill the remote worker to see if it fails gracefully await remote.dispose(); await vi.waitFor( async () => { try { const result = await env.SERVICE.ping(); throw new Error(`Expected error, got result: ${result}`); } catch (e) { expect(e instanceof Error ? e.message : `${e}`).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); } }, { timeout: 10_000, interval: 100 } ); }); test("fetch to durable object with remote running", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { fetch() { return new Response('Hello from Durable Object!'); } }; export default { async fetch(request, env, ctx) { return new Response("Hello from the default Worker Entrypoint!"); } } `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const ns = env.DO; const id = ns.newUniqueId(); const stub = ns.get(id); return stub.fetch(request); } } `, }); useDispose(local); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Hello from Durable Object!"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("RPC to durable object with remote running", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { ping() { return "pong"; } }; `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const ns = env.DO; const id = ns.newUniqueId(); const stub = ns.get(id); const result = await stub.ping(); return new Response(result); } catch (ex) { return new Response(ex.message, { status: 500 }); } } } `, }); useDispose(local); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("pong"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("fetch to durable object", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const ns = env.DO; const id = ns.newUniqueId(); const stub = ns.get(id); const response = await stub.fetch(request); return response; } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { fetch() { return new Response('Hello from Durable Object!'); } }; export default { async fetch(request, env, ctx) { return new Response("Hello from the default Worker Entrypoint!"); } } `, }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Hello from Durable Object!"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("RPC to durable object", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const ns = env.DO; const id = ns.newUniqueId(); const stub = ns.get(id); const result = await stub.ping(); return new Response("Response from remote worker: " + result); } catch (ex) { return new Response(ex.message, { status: 500 }); } } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toContain( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { ping() { return "pong"; } }; `, }); useDispose(remote); await remote.ready; // DO RPC now works natively via the debug port await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Response from remote worker: pong"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("workflow with cross-worker scriptName", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, workflows: { MY_WORKFLOW: { name: "MY_WORKFLOW", className: "MyWorkflow", scriptName: "remote-worker", }, }, compatibilityDate: "2024-11-20", modules: true, script: ` export default { async fetch(request, env, ctx) { const instance = await env.MY_WORKFLOW.create({ id: "cross-worker-instance" }); return Response.json({ id: instance.id }); } } `, }); useDispose(local); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, workflows: { MY_WORKFLOW: { name: "MY_WORKFLOW", className: "MyWorkflow", }, }, compatibilityDate: "2024-11-20", modules: true, script: ` import { WorkflowEntrypoint } from "cloudflare:workers"; export class MyWorkflow extends WorkflowEntrypoint { async run(event, step) { return await step.do("hello", async () => "from remote workflow"); } } export default { async fetch() { return new Response("ok"); } } `, }); useDispose(remote); await remote.ready; // Workflow `create()` is queued asynchronously by the engine; routing // goes engine (local) → ExternalServiceProxy → dev registry → remote // MyWorkflow.run. Verify the instance is created with the expected id. await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(res.status).toBe(200); expect(await res.json()).toEqual({ id: "cross-worker-instance" }); }, { timeout: 10_000, interval: 100 } ); }); test("DO RPC survives remote worker restart", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { ping() { return "v1"; } } export default { fetch() { return new Response("ok"); } } `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, // Use idFromName so the same DO instance is reused across requests — // this ensures the cached _cachedFetcher is exercised on the second call. script: ` export default { async fetch(request, env) { try { const id = env.DO.idFromName("stable"); const stub = env.DO.get(id); const result = await stub.ping(); return new Response("Response: " + result); } catch (ex) { return new Response(ex.message, { status: 500 }); } } } `, }); useDispose(local); // First request — caches the fetcher in the DO proxy instance await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Response: v1"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); // Restart remote — gets a new debug port, registry file updates await remote.setOptions({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { ping() { return "v2"; } } export default { fetch() { return new Response("ok"); } } `, }); // Second request — same DO instance (idFromName("stable")), // but remote debug port has changed. The cached _cachedFetcher // points to the old dead connection. await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Response: v2"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("service binding RPC survives remote worker restart", async ({ expect, }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { ping() { return "v1"; } } `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: "remote-worker", }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env) { try { const result = await env.SERVICE.ping(); return new Response("Response: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }); useDispose(local); // First request — ExternalServiceProxy resolves from registry await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Response: v1"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); // Restart remote — gets a new debug port, registry file updates await remote.setOptions({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { ping() { return "v2"; } } `, }); // Second request — ExternalServiceProxy is re-instantiated per request, // so it reads the updated registry Map and connects to the new debug port. await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Response: v2"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("scheduled to default entrypoint", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, unsafeTriggerHandlers: true, compatibilityFlags: ["experimental"], modules: true, script: ` let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); export default { async fetch() { try { const event = await Promise.race([ promise, new Promise((_, cancel) => setTimeout(cancel, 1000)) ]); return Response.json(event); } catch { return new Response("No scheduled event received", { status: 500 }); } }, scheduled(e) { resolve({ cron: e.cron, scheduledTime: e.scheduledTime }); } }; `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { REMOTE: "remote-worker", }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env) { await env.REMOTE.scheduled({ cron: "*/5 * * * *" }); return new Response("scheduled event dispatched"); } } `, }); useDispose(local); // Check scheduled() was dispatched await (await local.dispatchFetch("http://placeholder")).text(); const res = await remote.dispatchFetch("http://placeholder"); const json = (await res.json()) as { cron: string; scheduledTime: number }; expect(json).toMatchObject({ cron: "*/5 * * * *" }); expect(typeof json.scheduledTime).toBe("number"); }); test("tail to default entrypoint", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); export default { async fetch() { try { const event = await Promise.race([ promise, new Promise((_, cancel) => setTimeout(cancel, 1000)) ]); return Response.json(event[0].logs[0].message); } catch { return new Response("No tail event received", { status: 500 }); } }, tail(e) { resolve(e); } }; `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, tails: ["remote-worker"], serviceBindings: { remote: "remote-worker", }, handleRuntimeStdio: () => {}, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env) { if (request.url.includes("remote-worker")) { return env.remote.fetch(request) } console.log("DevReg: log event") return new Response("Hello from local-worker!"); } } `, }); useDispose(local); const res = await local.dispatchFetch("http://example.com"); const result = await res.text(); expect(result).toBe("Hello from local-worker!"); const res2 = await local.dispatchFetch("http://example.com/remote-worker"); const result2 = await res2.text(); expect(result2).toEqual(`["DevReg: log event"]`); }); test("tail to unknown worker", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const mf = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, tails: ["remote-worker"], serviceBindings: { remote: "remote-worker", }, compatibilityFlags: ["experimental"], modules: true, handleRuntimeStdio: () => {}, script: ` export default { async fetch(request, env) { if (request.url.includes("remote-worker")) { return env.remote.fetch(request) } console.log("DevReg: log event 2") return new Response("Hello from local-worker!"); } } `, }); useDispose(mf); const res = await mf.dispatchFetch("http://example.com"); const result = await res.text(); expect(result).toBe("Hello from local-worker!"); const res2 = await mf.dispatchFetch("http://example.com/remote-worker"); const result2 = await res2.text(); expect(result2).toEqual( `Worker "remote-worker" not found. Make sure it is running locally.` ); }); test("tail consumer props are propagated across instances", async ({ expect, }) => { const unsafeDevRegistryPath = await useTmp(); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; let resolve; const captured = new Promise((res) => { resolve = res; }); export default { async fetch() { try { const result = await Promise.race([ captured, new Promise((_, cancel) => setTimeout(() => cancel(new Error("no tail event received")), 2000) ) ]); return Response.json(result); } catch (e) { return new Response(e.message, { status: 500 }); } } }; export class TailCollector extends WorkerEntrypoint { async tail() { resolve({ props: this.ctx.props ?? null }); } } `, }); useDispose(remote); await remote.ready; const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, tails: [ { name: "remote-worker", entrypoint: "TailCollector", props: { tailKey: "from-tail-binding" }, }, ], handleRuntimeStdio: () => {}, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch() { console.log("DevReg: trigger tail event"); return new Response("ok"); } } `, }); useDispose(local); await vi.waitFor( async () => { // Fire a request that produces a log, which triggers a tail event // targeting the remote worker's TailCollector entrypoint. const trigger = await local.dispatchFetch("http://example.com"); expect(await trigger.text()).toBe("ok"); // Read the captured ctx.props back via the remote's default fetch. const res = await remote.dispatchFetch("http://placeholder"); const json = await res.json(); expect(res.status).toBe(200); expect(json).toEqual({ props: { tailKey: "from-tail-binding" }, }); }, { timeout: 10_000, interval: 100 } ); }); test("miniflare with different registry path", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const unsafeDevRegistryPath2 = await useTmp(); const localOptions: MiniflareOptions = { name: "local-worker", serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const result = await env.SERVICE.ping(); return new Response("Response from remote worker: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }; const remoteOptions: MiniflareOptions = { name: "remote-worker", compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }; const local = new Miniflare({ ...localOptions, unsafeDevRegistryPath, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); const remote = new Miniflare({ ...remoteOptions, unsafeDevRegistryPath, }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); const result = await res.text(); expect(result).toBe("Response from remote worker: pong"); }, { timeout: 10_000, interval: 100 } ); // Change remote's registry path to a different value await remote.setOptions({ ...remoteOptions, unsafeDevRegistryPath: unsafeDevRegistryPath2, }); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(500); }, { timeout: 10_000, interval: 100 } ); // Change local's registry path to the same path as remote's await local.setOptions({ ...localOptions, unsafeDevRegistryPath: unsafeDevRegistryPath2, }); await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); const result = await res.text(); expect(result).toBe("Response from remote worker: pong"); }, { timeout: 10_000, interval: 100 } ); }); test("fetch to module worker with https enabled", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const response = await env.SERVICE.fetch(request.url); const text = await response.text(); return new Response("Response from remote worker: " + text, { status: response.status, }); } } `, }); useDispose(local); const res = await local.dispatchFetch("https://example.com?name=World"); expect(await res.text()).toBe( `Response from remote worker: Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, https: true, script: ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); const name = url.searchParams.get("name") ?? 'anonymous'; return new Response("Hello " + name); } } `, // No direct sockets so that local will connect to the entry worker instead }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("https://example.com?name=World"); const result = await res.text(); expect(result).toBe("Response from remote worker: Hello World"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("fetch to durable object with https enabled", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, durableObjects: { DO: { className: "MyDurableObject", scriptName: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const ns = env.DO; const id = ns.newUniqueId(); const stub = ns.get(id); const response = await stub.fetch(request); return response; } } `, }); useDispose(local); const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe( `Worker "remote-worker" not found. Make sure it is running locally.` ); expect(res.status).toBe(503); const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, https: true, compatibilityFlags: ["experimental"], durableObjects: { DO: { className: "MyDurableObject", }, }, modules: true, script: ` import { DurableObject } from "cloudflare:workers"; export class MyDurableObject extends DurableObject { fetch() { return new Response('Hello from Durable Object!'); } }; export default { async fetch(request, env, ctx) { return new Response("Hello from the default Worker Entrypoint!"); } } `, }); useDispose(remote); await remote.ready; await vi.waitFor( async () => { const res = await local.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("Hello from Durable Object!"); expect(res.status).toBe(200); }, { timeout: 10_000, interval: 100 } ); }); test("handleDevRegistryUpdate callback", async ({ expect }) => { const unsafeDevRegistryPath = await useTmp(); const firstCallbackInvocations: Array<{ registry: WorkerRegistry; }> = []; const secondCallbackInvocations: Array<{ registry: WorkerRegistry; }> = []; // Create local Worker with service binding and callback const local = new Miniflare({ name: "local-worker", unsafeDevRegistryPath, unsafeHandleDevRegistryUpdate(registry) { firstCallbackInvocations.push({ registry }); }, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const result = await env.SERVICE.ping(); return new Response("Response from remote Worker: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }); useDispose(local); // Callback should not be triggered initially since no external services exist expect(firstCallbackInvocations.length).toBe(0); expect(secondCallbackInvocations.length).toBe(0); // Create an unrelated Worker - callback should NOT be triggered const unrelated = new Miniflare({ name: "unrelated-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch() { return new Response("Hello from unrelated-worker!"); } } `, }); useDispose(unrelated); await unrelated.ready; // Callback should not be triggered since we're not bound to unrelated-worker expect(firstCallbackInvocations.length).toBe(0); expect(secondCallbackInvocations.length).toBe(0); // Create remote worker (one we're actually bound to) - this should trigger the callback const remote = new Miniflare({ name: "remote-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export default class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }); onTestFinished(async () => { try { await remote.dispose(); } catch { // Ignore if already disposed } }); await remote.ready; // Wait for the callback to be triggered await vi.waitFor( async () => { // Callback should be triggered when bound Worker starts expect(firstCallbackInvocations.length).toBe(1); // Second callback should not be triggered yet expect(secondCallbackInvocations.length).toBe(0); }, { timeout: 10_000, interval: 100 } ); // Verify the callback was called with the correct registry data await vi.waitFor( async () => { const latestInvocation = firstCallbackInvocations.at(-1); // Registry should contain remote-worker expect(latestInvocation?.registry["remote-worker"]).toBeDefined(); }, { timeout: 10_000, interval: 100 } ); // Update unsafeHandleDevRegistryUpdate callback to push to a different array await local.setOptions({ name: "local-worker", unsafeDevRegistryPath, unsafeHandleDevRegistryUpdate(registry) { secondCallbackInvocations.push({ registry, }); }, serviceBindings: { SERVICE: { name: "remote-worker", }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { try { const result = await env.SERVICE.ping(); return new Response("Response from updated local Worker: " + result); } catch (e) { return new Response(e.message, { status: 500 }); } } } `, }); // Test disposal await remote.dispose(); // Wait for callback to be triggered by the update await vi.waitFor( async () => { // First callback should not be triggered again after update expect(firstCallbackInvocations.length).toBe(1); // Second callback should be triggered after update expect(secondCallbackInvocations.length).toBe(1); }, { timeout: 10_000, interval: 100 } ); // Verify if the remote worker is no longer in the registry await vi.waitFor( async () => { const latestInvocation = secondCallbackInvocations.at(-1); // Registry should not contain remote-worker expect(latestInvocation?.registry["remote-worker"]).toBeUndefined(); }, { timeout: 10_000, interval: 100 } ); }); test("advertises consumed queues in the worker's registry entry", async ({ expect, }) => { const unsafeDevRegistryPath = await useTmp(); const sharedOptions = { name: "consumer-worker", unsafeDevRegistryPath, compatibilityFlags: ["experimental"], modules: true, } satisfies Partial; const mf = new Miniflare({ ...sharedOptions, // Consumed queues are advertised on the worker's own registry entry so // producers in other processes can resolve this process's broker. queueConsumers: ["my-queue"], script: `export default { async queue() {} }`, }); useDispose(mf); await mf.ready; await vi.waitFor( () => { const registry = getWorkerRegistry(unsafeDevRegistryPath); expect(registry["consumer-worker"]?.queueConsumers).toEqual([ "my-queue", ]); }, { timeout: 10_000, interval: 100 } ); // Remove the consumer on reload so its advertisement is withdrawn and // doesn't misdirect producers. await mf.setOptions({ ...sharedOptions, script: `export default { async fetch() { return new Response("ok"); } }`, }); await vi.waitFor( () => { const registry = getWorkerRegistry(unsafeDevRegistryPath); // The worker's own entry is still advertised, without the queue. expect(registry["consumer-worker"]).toBeDefined(); expect(registry["consumer-worker"].queueConsumers).toBeUndefined(); }, { timeout: 10_000, interval: 100 } ); }); });