Files
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

1974 lines
50 KiB
TypeScript

import path from "node:path";
import { setTimeout } from "node:timers/promises";
import { mockConsoleMethods } from "@cloudflare/workers-utils/test-helpers";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import dedent from "ts-dedent";
import {
afterEach,
beforeEach,
describe,
it,
onTestFinished,
vi,
} from "vitest";
import {
importWrangler,
WranglerE2ETestHelper,
} from "./helpers/e2e-wrangler-test";
import type {
CloudflareWorkersModule,
D1Database,
DurableObjectNamespace,
ExportedHandler,
KVNamespace,
R2Bucket,
} from "@cloudflare/workers-types";
const { createTestHarness } = await importWrangler();
describe("createTestHarness", () => {
const logs = mockConsoleMethods();
let helper: WranglerE2ETestHelper;
function normalizeDebugOutput(output: string) {
return String(output)
.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/gm, "<timestamp>")
.replace(/http:\/\/127\.0\.0\.1:\d+/g, "http://127.0.0.1:<port>");
}
beforeEach(() => {
helper = new WranglerE2ETestHelper();
});
afterEach(() => {
vi.resetAllMocks();
});
it("starts with default server options", async ({ expect, onTestFailed }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "hello-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
export default {
fetch(request) {
if (new URL(request.url).pathname === "/url") {
return new Response(request.url);
}
return new Response("Hello World");
}
};
`,
});
const server = createTestHarness({
workers: [
{ configPath: path.resolve(helper.tmpPath, "./wrangler.jsonc") },
],
});
onTestFinished(server.close);
onTestFailed(server.debug);
const { url } = await server.listen();
expect(url.protocol).toBe("http:");
expect(url.hostname).toBe("127.0.0.1");
expect(Number(url.port)).toBeGreaterThan(0);
const response = await fetch(url);
await expect(response.text()).resolves.toBe("Hello World");
const relativeServerResponse = await server.fetch("/url");
await expect(relativeServerResponse.text()).resolves.toBe(
new URL("/url", url).href
);
const relativeWorkerResponse = await server.getWorker().fetch("/url");
await expect(relativeWorkerResponse.text()).resolves.toBe(
new URL("/url", url).href
);
});
it("can be configured after creation", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "delayed-config-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"vars": { "GREETING": "initial" }
}
`,
"src/index.ts": dedent`
export default {
fetch(_request, env) {
return new Response(env.GREETING);
}
};
`,
});
const server = createTestHarness();
onTestFinished(server.close);
await expect(server.listen()).rejects.toThrow(
"Test harness options have not been configured."
);
await server.update({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
await server.listen();
const initialResponse = await server.fetch("/");
await expect(initialResponse.text()).resolves.toBe("initial");
await server.update((options) => ({
...options,
workers: options.workers.map((worker) =>
"configPath" in worker
? { ...worker, vars: { GREETING: "updated" } }
: worker
),
}));
const updatedResponse = await server.fetch("/");
await expect(updatedResponse.text()).resolves.toBe("updated");
await server.reset();
const resetResponse = await server.fetch("/");
await expect(resetResponse.text()).resolves.toBe("initial");
});
it("support fetching different workers from the same session", async ({
expect,
}) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/primary.ts",
"compatibility_date": "2026-05-20"
}
`,
"wrangler.auxiliary.jsonc": dedent`
{
"name": "auxiliary-worker",
"main": "src/auxiliary.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/primary.ts": dedent`
export default {
fetch() {
return new Response("Hello from Primary Worker");
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch() {
return new Response("Hello from Auxiliary Worker");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{ configPath: "./wrangler.auxiliary.jsonc" },
],
});
onTestFinished(server.close);
await server.listen();
const defaultServerResponse = await server.fetch("/");
await expect(defaultServerResponse.text()).resolves.toBe(
"Hello from Primary Worker"
);
const defaultWorkerResponse = await server.getWorker().fetch("/");
await expect(defaultWorkerResponse.text()).resolves.toBe(
"Hello from Primary Worker"
);
const primaryResponse = await server.getWorker("primary-worker").fetch("/");
await expect(primaryResponse.text()).resolves.toBe(
"Hello from Primary Worker"
);
const auxiliaryResponse = await server
.getWorker("auxiliary-worker")
.fetch("/");
await expect(auxiliaryResponse.text()).resolves.toBe(
"Hello from Auxiliary Worker"
);
});
it("exposes resource bindings scoped to each worker environment", async ({
expect,
}) => {
await helper.seed({
"wrangler.api.jsonc": dedent`
{
"name": "api-worker",
"main": "src/api.ts",
"compatibility_date": "2026-05-20",
"vars": { "GREETING": "api" },
"kv_namespaces": [
{ "binding": "STORE", "id": "api-store" }
],
"r2_buckets": [
{ "binding": "BUCKET", "bucket_name": "api-bucket" }
]
}
`,
"wrangler.admin.jsonc": dedent`
{
"name": "admin-worker",
"main": "src/admin.ts",
"compatibility_date": "2026-05-20",
"vars": { "GREETING": "admin" },
"d1_databases": [
{
"binding": "DATABASE",
"database_name": "admin-database",
"database_id": "00000000-0000-0000-0000-000000000002"
}
],
"durable_objects": {
"bindings": [
{ "name": "OBJECT", "class_name": "Counter" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}
`,
"src/api.ts": dedent`
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/write-storage") {
await env.STORE.put("key", "api");
await env.BUCKET.put("key", "api");
return new Response("written");
}
return new Response("ok");
}
};
`,
"src/admin.ts": dedent`
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
async fetch() {
const value = (await this.ctx.storage.get("value")) ?? 0;
const next = value + 1;
await this.ctx.storage.put("value", next);
return new Response(String(next));
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/write-storage") {
await env.DATABASE.exec("CREATE TABLE entries (value TEXT);");
await env.DATABASE.prepare("INSERT INTO entries (value) VALUES (?);")
.bind("admin")
.run();
const id = env.OBJECT.idFromName("shared");
const stub = env.OBJECT.get(id);
return stub.fetch("http://example.com/");
}
return new Response("ok");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.api.jsonc",
secrets: { SECRET: "api-secret" },
},
{
configPath: "./wrangler.admin.jsonc",
secrets: { SECRET: "admin-secret" },
},
],
});
onTestFinished(server.close);
await server.listen();
const api = server.getWorker<{
GREETING: string;
SECRET: string;
STORE: KVNamespace;
BUCKET: R2Bucket;
}>("api-worker");
const admin = server.getWorker<{
GREETING: string;
SECRET: string;
DATABASE: D1Database;
OBJECT: DurableObjectNamespace;
}>("admin-worker");
const apiWriteResponse = await api.fetch("/write-storage");
expect(await apiWriteResponse.text()).toBe("written");
const adminWriteResponse = await admin.fetch("/write-storage");
expect(await adminWriteResponse.text()).toBe("1");
const apiEnv = await api.getEnv();
const adminEnv = await admin.getEnv();
expect(apiEnv.GREETING).toBe("api");
expect(apiEnv.SECRET).toBe("api-secret");
expect(adminEnv.GREETING).toBe("admin");
expect(adminEnv.SECRET).toBe("admin-secret");
// KV Namespace
await expect(apiEnv.STORE.get("key")).resolves.toBe("api");
// R2 Bucket
const object = await apiEnv.BUCKET.get("key");
if (object === null) {
expect.fail("Expected R2 object to exist");
} else {
await expect(object.text()).resolves.toBe("api");
}
// D1 Database
await expect(
adminEnv.DATABASE.prepare("SELECT value FROM entries").first("value")
).resolves.toBe("admin");
// Durable Object Namespace
const adminStub = adminEnv.OBJECT.get(adminEnv.OBJECT.idFromName("shared"));
const adminResponse = await adminStub.fetch("http://example.com/");
await expect(adminResponse.text()).resolves.toBe("2");
});
it("evicts Durable Objects by class name or binding name", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "do-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"exports": {
"Counter": {
"type": "durable-object",
"storage": "sqlite"
}
},
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter" }
]
}
}
`,
"src/index.ts": dedent`
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
memoryCount = 0;
async fetch() {
this.memoryCount += 1;
const storageCount = ((await this.ctx.storage.get("count")) ?? 0) + 1;
await this.ctx.storage.put("count", storageCount);
return Response.json({
memoryCount: this.memoryCount,
storageCount,
});
}
}
export default {
fetch(_request, _env, ctx) {
const id = ctx.exports.Counter.idFromName("user-123");
return ctx.exports.Counter.get(id).fetch("http://counter");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const worker = server.getWorker<
{ COUNTER: DurableObjectNamespace },
{
default: ExportedHandler<{ COUNTER: DurableObjectNamespace }>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Match runtime class exports by instance type.
Counter: new (...args: any[]) => CloudflareWorkersModule.DurableObject;
}
>("do-worker");
const response1 = await worker.fetch("/");
expect(await response1.json()).toEqual({
memoryCount: 1,
storageCount: 1,
});
// Evict by class name
await worker.evictDurableObject("Counter", { name: "user-123" });
const response2 = await worker.fetch("/");
expect(await response2.json()).toEqual({
memoryCount: 1,
storageCount: 2,
});
// Evict by binding name
await worker.evictDurableObject("COUNTER", { name: "user-123" });
const response3 = await worker.fetch("/");
expect(await response3.json()).toEqual({
memoryCount: 1,
storageCount: 3,
});
});
it("introspects Workflow instances by binding name", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "workflow-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"workflows": [
{
"binding": "MODERATOR",
"class_name": "ModeratorWorkflow",
"name": "moderator-workflow"
}
]
}
`,
"src/index.ts": dedent`
import { WorkflowEntrypoint } from "cloudflare:workers";
export class ModeratorWorkflow extends WorkflowEntrypoint {
async run(event, step) {
if (event.payload?.streamTest) {
const stream = await step.do("stream result", async () => {
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("real stream"));
controller.close();
}
});
});
return { streamResult: await new Response(stream).text() };
}
await step.sleep("sleep for a while", "10 seconds");
const result = await step.do("AI content scan", async () => {
const violationScore = Math.floor(Math.random() * 100);
return { violationScore };
});
if (result.violationScore < 10) {
await step.do("auto approve content", async () => {
return { status: "auto_approved" };
});
return { status: "auto_approved" };
}
await step.do("auto reject content", async () => {
return { status: "auto_rejected" };
});
return { status: "auto_rejected" };
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/moderate-known") {
const id = url.searchParams.get("id");
const workflow = await env.MODERATOR.create({ id });
return Response.json({ id: workflow.id });
}
if (url.pathname === "/moderate") {
const workflow = await env.MODERATOR.create();
return Response.json({
id: workflow.id,
details: await workflow.status()
});
}
if (url.pathname === "/moderate-batch") {
const workflows = await env.MODERATOR.createBatch([
{},
{ id: "321" },
{}
]);
return Response.json({ ids: workflows.map((workflow) => workflow.id) });
}
if (url.pathname === "/moderate-stream") {
const workflow = await env.MODERATOR.create({ params: { streamTest: true } });
return Response.json({ id: workflow.id });
}
return new Response("Not found", { status: 404 });
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const worker = server.getWorker("workflow-worker");
const stepName = "AI content scan";
const mockResult = { violationScore: 0 };
const instanceId = "known-workflow-instance";
const instance = await worker.introspectWorkflowInstance(
"MODERATOR",
instanceId
);
onTestFinished(instance.dispose);
await instance.modify(async (modifier) => {
await modifier.disableSleeps();
await modifier.mockStepResult({ name: stepName }, mockResult);
});
const response = await worker.fetch(`/moderate-known?id=${instanceId}`);
await expect(response.json()).resolves.toEqual({ id: instanceId });
await expect(
instance.waitForStepResult({ name: stepName })
).resolves.toEqual(mockResult);
await expect(instance.waitForStatus("complete")).resolves.not.toThrow();
await expect(instance.getOutput()).resolves.toEqual({
status: "auto_approved",
});
const workflow = await worker.introspectWorkflow("MODERATOR");
onTestFinished(workflow.dispose);
await expect(worker.introspectWorkflow("MODERATOR")).rejects.toThrow(
`Workflow "moderator-workflow" already has an active introspection session for binding "MODERATOR".`
);
await workflow.modifyAll(async (modifier) => {
await modifier.disableSleeps();
await modifier.mockStepResult({ name: stepName }, mockResult);
});
const unknownIdResponse = await worker.fetch("/moderate");
const unknownIdResult = (await unknownIdResponse.json()) as { id: string };
expect(unknownIdResult.id).not.toBe(instanceId);
const instances = await workflow.get();
expect(instances).toHaveLength(1);
await expect(
instances[0].waitForStepResult({ name: stepName })
).resolves.toEqual(mockResult);
await expect(instances[0].waitForStatus("complete")).resolves.not.toThrow();
await expect(instances[0].getOutput()).resolves.toEqual({
status: "auto_approved",
});
await workflow.dispose();
await expect(workflow.get()).rejects.toThrow(
`Workflow "moderator-workflow" does not have an active introspection session for this introspector.`
);
await expect(workflow.modifyAll(async () => {})).rejects.toThrow(
`Workflow "moderator-workflow" does not have an active introspection session for this introspector.`
);
const batchWorkflow = await worker.introspectWorkflow("MODERATOR");
onTestFinished(batchWorkflow.dispose);
await batchWorkflow.modifyAll(async (modifier) => {
await modifier.disableSleeps();
await modifier.mockStepResult({ name: stepName }, mockResult);
});
const batchResponse = await worker.fetch("/moderate-batch");
await expect(batchResponse.json()).resolves.toMatchObject({
ids: [expect.any(String), "321", expect.any(String)],
});
const batchInstances = await batchWorkflow.get();
expect(batchInstances).toHaveLength(3);
for (const batchInstance of batchInstances) {
await expect(
batchInstance.waitForStepResult({ name: stepName })
).resolves.toEqual(mockResult);
await expect(
batchInstance.waitForStatus("complete")
).resolves.not.toThrow();
await expect(batchInstance.getOutput()).resolves.toEqual({
status: "auto_approved",
});
}
await batchWorkflow.dispose();
const streamWorkflow = await worker.introspectWorkflow("MODERATOR");
onTestFinished(streamWorkflow.dispose);
await streamWorkflow.modifyAll(async (modifier) => {
await modifier.mockStepResult(
{ name: "stream result" },
new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode("mock stream"));
controller.close();
},
})
);
});
await worker.fetch("/moderate-stream");
const [streamInstance] = await streamWorkflow.get();
await expect(
streamInstance.waitForStatus("complete")
).resolves.not.toThrow();
await expect(streamInstance.getOutput()).resolves.toEqual({
streamResult: "mock stream",
});
});
it("applies D1 migrations to a worker binding", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "d1-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"d1_databases": [
{
"binding": "DATABASE",
"database_name": "test-database",
"database_id": "00000000-0000-0000-0000-000000000001",
"migrations_dir": "migrations",
"migrations_pattern": "migrations/*/migration.sql",
"migrations_table": "custom_migrations"
}
]
}
`,
"src/index.ts": dedent`
export default {
async fetch(request, env) {
const key = new URL(request.url).pathname.slice(1);
const row = await env.DATABASE.prepare("SELECT value FROM settings WHERE key = ?")
.bind(key)
.first();
if (row === null) {
return new Response("missing", { status: 404 });
}
return new Response(row.value);
}
};
`,
"migrations/0001_settings/migration.sql": dedent`
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
`,
"migrations/0002_seed/migration.sql": dedent`
INSERT INTO settings (key, value) VALUES ('greeting', 'Hello D1');
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const worker = server.getWorker<{ DATABASE: D1Database }>();
await worker.applyD1Migrations("DATABASE");
const response = await worker.fetch("/greeting");
await expect(response.text()).resolves.toBe("Hello D1");
await worker.applyD1Migrations("DATABASE");
const secondResponse = await worker.fetch("/greeting");
await expect(secondResponse.text()).resolves.toBe("Hello D1");
server.debug();
const debugOutput = normalizeDebugOutput(logs.getAndClearOut());
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - applied 0001_settings/migration.sql"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - applied 0002_seed/migration.sql"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - completed (2 applied)"
);
expect(debugOutput).toContain(
"[server] [d1-worker] d1 migrations - DATABASE - completed (no migrations to apply)"
);
});
it("supports service bindings between workers", async ({ expect }) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/primary.ts",
"compatibility_date": "2026-05-20",
"services": [
{ "binding": "AUXILIARY", "service": "auxiliary-worker" }
]
}
`,
"wrangler.auxiliary.jsonc": dedent`
{
"name": "auxiliary-worker",
"main": "src/auxiliary.ts",
"compatibility_date": "2026-05-20",
"services": [
{ "binding": "PRIMARY", "service": "primary-worker" }
]
}
`,
"src/primary.ts": dedent`
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/call-auxiliary") {
return env.AUXILIARY.fetch("http://auxiliary.example.com/from-primary");
}
return new Response("primary:" + url.pathname);
}
};
`,
"src/auxiliary.ts": dedent`
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/call-primary") {
return env.PRIMARY.fetch("http://primary.example.com/from-auxiliary");
}
return new Response("auxiliary:" + url.pathname);
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{ configPath: "./wrangler.auxiliary.jsonc" },
],
});
onTestFinished(server.close);
await server.listen();
const auxiliaryResponse = await server
.getWorker("primary-worker")
.fetch("/call-auxiliary");
await expect(auxiliaryResponse.text()).resolves.toBe(
"auxiliary:/from-primary"
);
const primaryResponse = await server
.getWorker("auxiliary-worker")
.fetch("/call-primary");
await expect(primaryResponse.text()).resolves.toBe(
"primary:/from-auxiliary"
);
});
it("supports binding overrides for first-party bindings", async ({
expect,
}) => {
await helper.seed({
"wrangler.app.jsonc": dedent`
{
"name": "app-worker",
"main": "src/app.ts",
"compatibility_date": "2026-05-20",
"ai": { "binding": "AI" }
}
`,
"src/app.ts": dedent`
export default {
async fetch(_request, env) {
return Response.json(await env.AI.run("@cf/test/model", {
prompt: "What should the test return?"
}));
}
};
`,
"src/mock-ai.ts": dedent`
import { WorkerEntrypoint } from "cloudflare:workers";
let calls = 0;
export default class MockAi extends WorkerEntrypoint {
run(model, options) {
calls += 1;
return {
model,
response: "mock-ai:" + calls,
prompt: options.prompt,
};
}
}
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.app.jsonc",
bindingOverrides: {
AI: "mock-ai",
},
},
{
config: {
name: "mock-ai",
main: "src/mock-ai.ts",
compatibility_date: "2026-05-20",
},
},
],
});
onTestFinished(server.close);
await server.listen();
const appResponse = await server.getWorker("app-worker").fetch("/");
await expect(appResponse.json()).resolves.toEqual({
model: "@cf/test/model",
prompt: "What should the test return?",
response: "mock-ai:1",
});
const secondAppResponse = await server.getWorker("app-worker").fetch("/");
await expect(secondAppResponse.json()).resolves.toEqual({
model: "@cf/test/model",
prompt: "What should the test return?",
response: "mock-ai:2",
});
await server.reset();
const resetAppResponse = await server.getWorker("app-worker").fetch("/");
await expect(resetAppResponse.json()).resolves.toEqual({
model: "@cf/test/model",
prompt: "What should the test return?",
response: "mock-ai:1",
});
});
it("returns the default worker export", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "rpc-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
import { WorkerEntrypoint } from "cloudflare:workers";
export default class RpcWorker extends WorkerEntrypoint {
getMessage(name) {
return "Hello " + name;
}
}
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
interface RpcWorker extends CloudflareWorkersModule.WorkerEntrypoint {
getMessage(name: string): string;
}
type RpcWorkerConstructor = new (
...args: ConstructorParameters<
typeof CloudflareWorkersModule.WorkerEntrypoint
>
) => RpcWorker;
type RpcWorkerModule = { default: RpcWorkerConstructor };
const rpcWorker = server.getWorker<unknown, RpcWorkerModule>("rpc-worker");
const rpcExport = await rpcWorker.getExport();
expect(await rpcExport.getMessage("World")).toBe("Hello World");
});
it("uses runtime errors for missing binding override targets", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "app-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"ai": { "binding": "AI" }
}
`,
"src/index.ts": dedent`
export default {
async fetch(_request, env) {
return Response.json(await env.AI.run("@cf/test/model", {}));
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.jsonc",
bindingOverrides: { AI: "missing-ai" },
},
],
});
onTestFinished(server.close);
await expect(server.listen()).rejects.toThrow("core:user:missing-ai");
});
it("routes fetches based on worker routes", async ({ expect }) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/primary.ts",
"compatibility_date": "2026-05-20",
"routes": ["primary.example.com/*"]
}
`,
"src/primary.ts": dedent`
export default {
fetch(request) {
return Response.json({ name: "primary", url: request.url });
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch(request) {
return Response.json({ name: "auxiliary", url: request.url });
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{
config: {
name: "auxiliary-worker",
main: "src/auxiliary.ts",
compatibility_date: "2026-05-20",
routes: ["auxiliary.example.com/*"],
},
},
],
});
onTestFinished(server.close);
await server.listen();
const primaryResponse = await server.fetch(
"http://primary.example.com/path?value=1"
);
await expect(primaryResponse.json()).resolves.toEqual({
name: "primary",
url: "http://primary.example.com/path?value=1",
});
const auxiliaryResponse = await server.fetch(
"http://auxiliary.example.com/path?value=2"
);
await expect(auxiliaryResponse.json()).resolves.toEqual({
name: "auxiliary",
url: "http://auxiliary.example.com/path?value=2",
});
await server.update({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{
config: {
name: "auxiliary-worker",
main: "src/auxiliary.ts",
compatibility_date: "2026-05-20",
routes: ["updated-auxiliary.example.com/*"],
},
},
],
});
const updatedAuxiliaryResponse = await server.fetch(
"http://updated-auxiliary.example.com/path?value=3"
);
await expect(updatedAuxiliaryResponse.json()).resolves.toEqual({
name: "auxiliary",
url: "http://updated-auxiliary.example.com/path?value=3",
});
await server.reset();
const resetAuxiliaryResponse = await server.fetch(
"http://auxiliary.example.com/path?value=4"
);
await expect(resetAuxiliaryResponse.json()).resolves.toEqual({
name: "auxiliary",
url: "http://auxiliary.example.com/path?value=4",
});
});
it("rejects updates that change the number of workers without committing them", async ({
expect,
}) => {
await helper.seed({
"src/primary.ts": dedent`
export default {
fetch() {
return new Response("primary");
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch() {
return new Response("auxiliary");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
config: {
name: "primary-worker",
main: "src/primary.ts",
compatibility_date: "2026-05-20",
},
},
],
});
onTestFinished(server.close);
await server.listen();
await expect(
server.update((options) => ({
...options,
workers: [
...options.workers,
{
config: {
name: "auxiliary-worker",
main: "src/auxiliary.ts",
compatibility_date: "2026-05-20",
},
},
],
}))
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Updating the number of workers running in the server is not supported.]`
);
let workerCountAfterRejectedUpdate = 0;
await server.update((options) => {
workerCountAfterRejectedUpdate = options.workers.length;
return options;
});
expect(workerCountAfterRejectedUpdate).toBe(1);
});
it("rejects update when a worker fails to build", async ({ expect }) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/primary.ts",
"compatibility_date": "2026-05-20"
}
`,
"wrangler.auxiliary.jsonc": dedent`
{
"name": "auxiliary-worker",
"main": "src/auxiliary.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/primary.ts": dedent`
export default {
fetch() {
return new Response("Hello from Primary Worker");
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch() {
return new Response("Hello from Auxiliary Worker");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{ configPath: "./wrangler.auxiliary.jsonc" },
],
});
onTestFinished(server.close);
await server.listen();
// Either of these changes should cause a build failure
await helper.seed({
"src/primary.ts": dedent`
export default {
fetch() {
return new Response("broken);
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch() {
return new Response("broken too";
}
};
`,
});
await expect(
Promise.race([
server.update((options) => options),
setTimeout(5_000).then(() => {
throw new Error("server.update() timed out");
}),
])
).rejects.toThrow("Build failed");
});
it("rejects calls on unknown worker handles", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
export default {
fetch() {
return new Response("primary");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
await expect(
server.getWorker("unknown-worker").fetch("/")
).rejects.toThrowErrorMatchingInlineSnapshot(
`[TypeError: Worker "unknown-worker" does not exist in this server.]`
);
await expect(
server.getWorker("unknown-worker").scheduled({
cron: "0 0 * * *",
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`[TypeError: Worker "unknown-worker" does not exist in this server.]`
);
});
it("uses the current Node process fetch for outbound requests by default", async ({
expect,
}) => {
const mockServer = setupServer(
http.get("http://example.com/", () => {
return HttpResponse.text("Mocked by MSW");
})
);
mockServer.listen({ onUnhandledRequest: "error" });
onTestFinished(() => mockServer.close());
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "default-outbound-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
export default {
fetch() {
return fetch("http://example.com");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.text()).resolves.toBe("Mocked by MSW");
});
it("starts workers from inline config", async ({ expect }) => {
await helper.seed({
"src/index.ts": dedent`
export default {
fetch() {
return new Response("Hello from inline config");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
config: {
main: "src/index.ts",
compatibility_date: "2026-05-20",
},
},
],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.text()).resolves.toBe("Hello from inline config");
});
it("loads default .env files for config path workers", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "env-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"vars": { "CONFIG_VAR": "from-config" }
}
`,
".env": dedent`
ENV_SECRET=from-env
`,
"src/index.ts": dedent`
export default {
fetch(request, env) {
return Response.json({
CONFIG_VAR: env.CONFIG_VAR,
ENV_SECRET: env.ENV_SECRET,
});
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.json()).resolves.toEqual({
CONFIG_VAR: "from-config",
ENV_SECRET: "from-env",
});
});
it("loads default .dev.vars files for config path workers", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "dev-vars-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"vars": { "CONFIG_VAR": "from-config" }
}
`,
".env": dedent`
SECRET=from-env
`,
".dev.vars": dedent`
SECRET=from-dev-vars
`,
"src/index.ts": dedent`
export default {
fetch(request, env) {
return Response.json({
CONFIG_VAR: env.CONFIG_VAR,
SECRET: env.SECRET,
});
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.json()).resolves.toEqual({
CONFIG_VAR: "from-config",
SECRET: "from-dev-vars",
});
});
it("overrides vars and secrets for config path workers", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "var-overrides-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"vars": { "CONFIG_VAR": "from-config" },
"secrets": { "required": ["API_TOKEN", "SECRET_FROM_FILE"] }
}
`,
".dev.vars": dedent`
API_TOKEN=from-dev-vars
SECRET_FROM_FILE=from-dev-vars
`,
"src/index.ts": dedent`
export default {
fetch(request, env) {
return Response.json({
CONFIG_VAR: env.CONFIG_VAR,
API_TOKEN: env.API_TOKEN,
SECRET_FROM_FILE: env.SECRET_FROM_FILE,
ADDED_VAR: env.ADDED_VAR,
NULL_VAR: env.NULL_VAR,
});
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.jsonc",
vars: {
CONFIG_VAR: "from-override",
ADDED_VAR: "from-override",
NULL_VAR: null,
},
secrets: {
API_TOKEN: "from-override",
},
},
],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.json()).resolves.toEqual({
CONFIG_VAR: "from-override",
API_TOKEN: "from-override",
SECRET_FROM_FILE: "from-dev-vars",
ADDED_VAR: "from-override",
NULL_VAR: null,
});
await server.update({
root: helper.tmpPath,
workers: [
{
configPath: "./wrangler.jsonc",
vars: {
CONFIG_VAR: "from-updated-override",
ADDED_VAR: "from-updated-override",
NULL_VAR: null,
},
secrets: {
API_TOKEN: "from-updated-override",
},
},
],
});
const updatedResponse = await server.fetch("/");
await expect(updatedResponse.json()).resolves.toEqual({
CONFIG_VAR: "from-updated-override",
API_TOKEN: "from-updated-override",
SECRET_FROM_FILE: "from-dev-vars",
ADDED_VAR: "from-updated-override",
NULL_VAR: null,
});
});
it(`supports "nodejs_compat" flag`, async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "nodejs-compat-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"compatibility_flags": ["nodejs_compat"]
}
`,
"src/index.ts": dedent`
import { Stream } from "node:stream";
export default {
fetch() {
return new Response(String(typeof Stream));
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const response = await server.fetch("/");
await expect(response.text()).resolves.toBe("function");
});
it("uses ephemeral storage by default", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "ephemeral-storage-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"kv_namespaces": [
{ "binding": "STORE", "id": "test-store" }
]
}
`,
"src/index.ts": dedent`
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/set") {
await env.STORE.put("key", "value");
return new Response("stored");
}
return new Response((await env.STORE.get("key")) ?? "missing");
}
};
`,
});
const firstServer = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(firstServer.close);
await firstServer.listen();
const setResponse = await firstServer.fetch("/set");
await expect(setResponse.text()).resolves.toBe("stored");
const storedResponse = await firstServer.fetch("/");
await expect(storedResponse.text()).resolves.toBe("value");
await firstServer.close();
const secondServer = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(secondServer.close);
await secondServer.listen();
const resetResponse = await secondServer.fetch("/");
await expect(resetResponse.text()).resolves.toBe("missing");
});
it("resets server options and restarts the session", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "storage-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"kv_namespaces": [
{ "binding": "STORE", "id": "test-store" }
]
}
`,
"src/index.ts": dedent`
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/set") {
await env.STORE.put("key", "value");
return new Response("stored");
}
return new Response((await env.STORE.get("key")) ?? "missing");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await expect(server.reset()).rejects.toThrow(
"Server has not been started. Start it with server.listen() before calling this method."
);
await server.listen();
const setResponse = await server.fetch("/set");
await expect(setResponse.text()).resolves.toBe("stored");
const storedResponse = await server.fetch("/");
await expect(storedResponse.text()).resolves.toBe("value");
await server.reset();
const resetResponse = await server.fetch("/");
await expect(resetResponse.text()).resolves.toBe("missing");
});
it("triggers scheduled handlers", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
let lastCron = "missing";
export default {
fetch() {
return new Response(lastCron);
},
scheduled(event) {
lastCron = event.cron;
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const beforeScheduled = await server.fetch("/");
await expect(beforeScheduled.text()).resolves.toBe("missing");
await expect(
server.getWorker().scheduled({
cron: "* * * * *",
scheduledTime: new Date(1_700_000_100_000),
})
).resolves.toEqual({ outcome: "ok", noRetry: false });
const afterScheduled = await server.fetch("/");
await expect(afterScheduled.text()).resolves.toBe("* * * * *");
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed
<timestamp> [server] fetch - GET / - started
<timestamp> [server] fetch - GET / - 200
<timestamp> [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - started
<timestamp> [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - 200
<timestamp> [server] fetch - GET / - started
<timestamp> [server] fetch - GET / - 200"
`);
});
it("lists Durable Object ids by class name or binding name", async ({
expect,
}) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "durable-object-list-worker",
"main": "src/index.ts",
"compatibility_date": "2026-05-20",
"durable_objects": {
"bindings": [
{ "name": "COUNTER", "class_name": "Counter" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["Counter"] }
]
}
`,
"src/index.ts": dedent`
export class Counter {
constructor(ctx) {
this.ctx = ctx;
}
async fetch() {
await this.ctx.storage.put("count", 1);
return new Response("stored");
}
}
export default {
fetch(request, env) {
const name = new URL(request.url).searchParams.get("name") ?? "default";
const id = env.COUNTER.idFromName(name);
return env.COUNTER.get(id).fetch("https://counter.example");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const worker = server.getWorker<
{ COUNTER: DurableObjectNamespace },
{
default: ExportedHandler<{ COUNTER: DurableObjectNamespace }>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Match runtime class exports by instance type.
Counter: new (...args: any[]) => CloudflareWorkersModule.DurableObject;
}
>();
const env = await worker.getEnv();
const firstId = env.COUNTER.idFromName("first").toString();
const secondId = env.COUNTER.idFromName("second").toString();
await expect(worker.listDurableObjectIds("COUNTER")).resolves.toEqual([]);
await expect(server.fetch("/?name=first")).resolves.toHaveProperty(
"status",
200
);
await expect(server.fetch("/?name=second")).resolves.toHaveProperty(
"status",
200
);
await expect(worker.listDurableObjectIds("Counter")).resolves.toEqual(
[firstId, secondId].sort()
);
await expect(worker.listDurableObjectIds("COUNTER")).resolves.toEqual(
[firstId, secondId].sort()
);
});
it("does not reload on source changes by default", async ({ expect }) => {
await helper.seed({
"wrangler.jsonc": dedent`
{
"name": "create-server-test",
"main": "src/index.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/index.ts": dedent`
export default {
fetch() {
return new Response("Hello World");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [{ configPath: "./wrangler.jsonc" }],
});
onTestFinished(server.close);
await server.listen();
const response1 = await server.fetch("/");
await expect(response1.text()).resolves.toBe("Hello World");
await helper.seed({
"src/index.ts": dedent`
export default {
fetch() {
return new Response("Greeting");
}
};
`,
});
// Wait a moment to ensure that if the server were going to reload, it would have done so by now
await setTimeout(1000);
const response2 = await server.fetch("/");
await expect(response2.text()).resolves.toBe("Hello World");
await helper.seed({
"src/index.ts": dedent`
export default {
fetch() {
return new Response("Bonjour");
}
};
`,
});
await setTimeout(1000);
const response3 = await server.fetch("/");
await expect(response3.text()).resolves.toBe("Hello World");
});
it("captures runtime logs and prints debug timelines", async ({ expect }) => {
await helper.seed({
"wrangler.primary.jsonc": dedent`
{
"name": "primary-worker",
"main": "src/primary.ts",
"compatibility_date": "2026-05-20"
}
`,
"wrangler.auxiliary.jsonc": dedent`
{
"name": "auxiliary-worker",
"main": "src/auxiliary.ts",
"compatibility_date": "2026-05-20"
}
`,
"src/primary.ts": dedent`
export default {
fetch() {
console.info("primary log");
return new Response("primary ok");
}
};
`,
"src/auxiliary.ts": dedent`
export default {
fetch() {
console.warn("auxiliary warning");
throw new Error("auxiliary failed");
}
};
`,
});
const server = createTestHarness({
root: helper.tmpPath,
workers: [
{ configPath: "./wrangler.primary.jsonc" },
{ configPath: "./wrangler.auxiliary.jsonc" },
],
});
onTestFinished(server.close);
server.debug();
expect(logs.getAndClearOut()).toMatchInlineSnapshot(
`"-------------- No debug log --------------"`
);
await server.listen();
expect(server.getLogs()).toEqual([]);
server.clearLogs();
const primaryResponse = await server.getWorker("primary-worker").fetch("/");
await expect(primaryResponse.text()).resolves.toBe("primary ok");
await expect(
server
.getWorker("auxiliary-worker")
.fetch("https://example.com/greet", { method: "post" })
).rejects.toThrow("auxiliary failed");
expect(server.getLogs()).toEqual([
{ timestamp: expect.any(Number), level: "info", message: "primary log" },
{
timestamp: expect.any(Number),
level: "warn",
message: "auxiliary warning",
},
]);
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed
<timestamp> [server] [primary-worker] fetch - GET / - started
<timestamp> [runtime] info: primary log
<timestamp> [server] [primary-worker] fetch - GET / - 200
<timestamp> [server] [auxiliary-worker] fetch - POST https://example.com/greet - started
<timestamp> [runtime] warn: auxiliary warning
<timestamp> [server] [auxiliary-worker] fetch - POST https://example.com/greet - failed"
`);
server.clearLogs();
expect(server.getLogs()).toEqual([]);
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed
<timestamp> [server] [primary-worker] fetch - GET / - started
<timestamp> [runtime] info: primary log
<timestamp> [server] [primary-worker] fetch - GET / - 200
<timestamp> [server] [auxiliary-worker] fetch - POST https://example.com/greet - started
<timestamp> [runtime] warn: auxiliary warning
<timestamp> [server] [auxiliary-worker] fetch - POST https://example.com/greet - failed"
`);
await server.reset();
expect(server.getLogs()).toEqual([]);
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed"
`);
await server.update((options) => ({
...options,
workers: [...options.workers],
}));
expect(server.getLogs()).toEqual([]);
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed
<timestamp> [server] update - started
<timestamp> [server] update - completed"
`);
// Make sure debug logs are still visible after server is closed
await server.close();
server.debug();
expect(normalizeDebugOutput(logs.getAndClearOut())).toMatchInlineSnapshot(`
"--------------- debug logs ---------------
<timestamp> [server] startup - started
<timestamp> [server] startup - completed
<timestamp> [server] update - started
<timestamp> [server] update - completed
<timestamp> [server] teardown - started
<timestamp> [server] teardown - completed"
`);
});
});