1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Plugin from "../Plugin.ts";
5import * as PluginContext from "../PluginContext.ts";
6import type { RuntimeWorker } from "../RuntimeWorker.ts";
7import { SERVICE_USER_WORKER } from "../internal/constants.ts";
8
9const makeWorker = (overrides: Partial<RuntimeWorker> = {}): RuntimeWorker => ({
10 name: "test",
11 compatibilityDate: "2026-03-10",
12 compatibilityFlags: [],
13 bindings: [],
14 modules: [],
15 ...overrides,
16});
17
18class Greeter extends Plugin.Service<
19 Greeter,
20 { greet: (name: string) => string }
21>()("cloudflare-runtime/plugin/Greeter") {}
22
23const GreeterLive = Layer.succeed(
24 Greeter,
25 Greeter.of({
26 services: [{ name: "greeter:svc" }],
27 api: { greet: (name) => `hello, ${name}!` },
28 }),
29);
30
31describe("Plugin / PluginContext", () => {
32 it.effect("registers plugins discovered in the Effect context", () =>
33 Effect.gen(function* () {
34 const ctx = yield* PluginContext.make(makeWorker());
35 expect(ctx.plugins.size).toBe(1);
36 const greeter = yield* ctx.get(Greeter);
37 expect(greeter.api.greet("world")).toBe("hello, world!");
38 const config = yield* ctx.config;
39 expect(config.services).toEqual([{ name: "greeter:svc" }]);
40 }).pipe(Effect.provide(GreeterLive)),
41 );
42
43 it.effect("get fails with a ConfigError when a plugin is missing", () =>
44 Effect.gen(function* () {
45 const ctx = yield* PluginContext.make(makeWorker());
46 const error = yield* ctx.get(Greeter).pipe(Effect.flip);
47 expect(error).toMatchObject({
48 _tag: "ConfigError",
49 subtag: "PluginNotFound",
50 });
51 }),
52 );
53
54 it.effect("middlewares chain into a service pipeline ending at 'user'", () =>
55 Effect.gen(function* () {
56 class A extends Plugin.Service<A>()("cloudflare-runtime/plugin/A") {}
57 class B extends Plugin.Service<B>()("cloudflare-runtime/plugin/B") {}
58 const layerA = Layer.succeed(
59 A,
60 A.of({
61 middlewares: [
62 {
63 name: "mw:a",
64 worker: { compatibilityDate: "2026-03-10" },
65 upstreamBindingName: "NEXT",
66 },
67 ],
68 }),
69 );
70 const layerB = Layer.succeed(
71 B,
72 B.of({
73 middlewares: [
74 {
75 name: "mw:b",
76 worker: { compatibilityDate: "2026-03-10" },
77 upstreamBindingName: "NEXT",
78 },
79 ],
80 }),
81 );
82 const ctx = yield* PluginContext.make(makeWorker()).pipe(
83 Effect.provide(Layer.mergeAll(layerA, layerB)),
84 );
85 const config = yield* ctx.config;
86 expect(config.entry === "mw:a" || config.entry === "mw:b").toBe(true);
87 const middlewareServices = config.services.filter((s) =>
88 s.name?.startsWith("mw:"),
89 );
90 expect(middlewareServices).toHaveLength(2);
91 const lastMiddleware = middlewareServices[middlewareServices.length - 1];
92 const lastBindings = (
93 lastMiddleware as { worker: { bindings: Array<any> } }
94 ).worker.bindings;
95 const upstream = lastBindings.find((b) => b.name === "NEXT");
96 expect(upstream?.service?.name).toBe(SERVICE_USER_WORKER);
97 }),
98 );
99
100 it.effect("Plugin.useSync reads a plugin field synchronously", () =>
101 Effect.gen(function* () {
102 const ctx = yield* PluginContext.make(makeWorker());
103 const greeting = yield* Plugin.useSync(Greeter, (g) =>
104 g.api.greet("ctx"),
105 ).pipe(Effect.provideService(PluginContext.PluginContext, ctx));
106 expect(greeting).toBe("hello, ctx!");
107 }).pipe(Effect.provide(GreeterLive)),
108 );
109});