1import { expect, layer } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as WorkerProxy from "../proxy/WorkerProxy.ts";
5import * as Runtime from "../Runtime.ts";
6import { localRuntimeLayer, PredicateFailed } from "./helpers/runtime.ts";
7
8const HELLO_SCRIPT = `
9export default {
10 fetch(request) {
11 const url = new URL(request.url);
12 if (url.pathname === "/hello") return new Response("hello");
13 if (url.pathname === "/echo") return new Response(request.method);
14 return new Response("not found", { status: 404 });
15 }
16};
17`;
18
19layer(localRuntimeLayer, { excludeTestServices: true })("Runtime", (it) => {
20 it.effect(
21 "starts a worker and returns an http://127.0.0.1:<port> URL",
22 () =>
23 Effect.gen(function* () {
24 const runtime = yield* Runtime.Runtime;
25 const url = yield* runtime.start({
26 name: "smoke",
27 compatibilityDate: "2026-03-10",
28 compatibilityFlags: [],
29 bindings: [],
30 modules: [
31 { name: "main.js", type: "ESModule", content: HELLO_SCRIPT },
32 ],
33 });
34 expect(url).toBeInstanceOf(URL);
35 expect(url.href).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/$/);
36 const hello = yield* Effect.promise(() =>
37 fetch(new URL("/hello", url)),
38 );
39 expect(hello.status).toBe(200);
40 expect(yield* Effect.promise(() => hello.text())).toBe("hello");
41 const echo = yield* Effect.promise(() =>
42 fetch(new URL("/echo", url), { method: "POST" }),
43 );
44 expect(yield* Effect.promise(() => echo.text())).toBe("POST");
45 }),
46 { timeout: 30_000 },
47 );
48
49 it.effect("surfaces user-script errors as ConfigError", () =>
50 Effect.gen(function* () {
51 const runtime = yield* Runtime.Runtime;
52 const error = yield* runtime
53 .start({
54 name: "broken",
55 compatibilityDate: "2026-03-10",
56 compatibilityFlags: [],
57 bindings: [],
58 modules: [
59 {
60 name: "main.js",
61 type: "ESModule",
62 content: "this is not valid javascript ===",
63 },
64 ],
65 })
66 .pipe(Effect.flip);
67 expect(error._tag).toBe("ConfigError");
68 }),
69 );
70 it.effect(
71 "starts many workers concurrently, including proxy",
72 () =>
73 Effect.gen(function* () {
74 const runtime = yield* Runtime.Runtime;
75 const proxy = yield* WorkerProxy.WorkerProxy;
76
77 const start = (index: number) =>
78 Effect.gen(function* () {
79 const instance = yield* proxy.serve();
80 const worker = yield* runtime.start({
81 name: `smoke-${index}`,
82 compatibilityDate: "2026-03-10",
83 compatibilityFlags: [],
84 bindings: [],
85 modules: [
86 { name: "main.js", type: "ESModule", content: HELLO_SCRIPT },
87 ],
88 });
89 yield* instance.set(worker);
90 return instance.url;
91 });
92
93 const count = 25;
94 const urls = yield* Effect.all(
95 Array.from({ length: count }, (_, index) => start(index)),
96 { concurrency: "unbounded" },
97 );
98 expect(new Set(urls).size).toBe(count);
99 }),
100 { timeout: 60_000 },
101 );
102
103 it.effect(
104 "surfaces uncaught worker exceptions via logging.verbose + onOutput",
105 () =>
106 Effect.gen(function* () {
107 const runtime = yield* Runtime.Runtime;
108 let captured = "";
109 const url = yield* runtime.start({
110 name: "throwing",
111 compatibilityDate: "2026-03-10",
112 compatibilityFlags: [],
113 bindings: [],
114 modules: [
115 {
116 name: "main.js",
117 type: "ESModule",
118 content: `
119 export default {
120 fetch() {
121 throw new Error("kaboom-marker");
122 },
123 };
124 `,
125 },
126 ],
127 logging: {
128 verbose: true,
129 onOutput: (chunk) => {
130 captured += chunk;
131 },
132 },
133 });
134 const res = yield* Effect.promise(() => fetch(new URL("/", url)));
135 expect(res.status).toBe(500);
136 // workerd writes the exception asynchronously; wait for it to land.
137 yield* Effect.suspend(() =>
138 captured.includes("kaboom-marker")
139 ? Effect.void
140 : Effect.fail(new PredicateFailed({ value: captured })),
141 ).pipe(
142 Effect.retry({
143 while: (error) => error._tag === "PredicateFailed",
144 schedule: Schedule.spaced("50 millis"),
145 times: 100,
146 }),
147 );
148 expect(captured).toContain("kaboom-marker");
149 }),
150 { timeout: 30_000 },
151 );
152});