1/**
2 * A tagged platform resource declared as a bare tag — no props, no inline
3 * impl — gets both from its `.make(props, impl)` Layer. Yielding it without
4 * that Layer used to register the resource with `undefined` props, so the
5 * failure surfaced much later as an opaque `TypeError` inside whichever
6 * provider first read a prop (`news.name` in the Cloudflare Worker
7 * pre-create). It must fail fast instead, naming the class and its Layer.
8 *
9 * The three neighbouring branches of `Platform.ts`'s tag handling must keep
10 * working: tag + Layer provided, tag + props (external), plain non-tagged.
11 */
12import * as Alchemy from "@/index.ts";
13import { Platform } from "@/Platform.ts";
14import * as Provider from "@/Provider.ts";
15import type { Resource } from "@/Resource.ts";
16import { InMemoryService, State } from "@/State/index.ts";
17import * as Test from "@/Test/Alchemy";
18import { describe, expect } from "alchemy-test";
19import * as Effect from "effect/Effect";
20import * as Exit from "effect/Exit";
21import * as Layer from "effect/Layer";
22
23interface Widget extends Resource<
24 "Test.PlatformWidget",
25 { name?: string; env?: Record<string, any>; exports?: string[] },
26 { name: string },
27 { env?: Record<string, any> }
28> {}
29
30const Widget: any = Platform<Widget>("Test.PlatformWidget", {
31 createRuntimeContext: () => ({}) as any,
32});
33
34/** `Widget` is untyped (`Platform` returns `any`), so yields need a cast. */
35const yieldWidget = (cls: unknown) => cls as Effect.Effect<{ name: string }>;
36
37/** What the provider received as `news`, if it ran at all. */
38let observed: { ran: boolean; news: unknown } = { ran: false, news: undefined };
39
40const providers = Provider.succeed(Widget, {
41 list: () => Effect.succeed([]),
42 diff: Effect.fn(function* () {
43 return undefined;
44 }),
45 reconcile: Effect.fn(function* ({ id, news }: any) {
46 observed = { ran: true, news };
47 return { name: news?.name ?? id };
48 }),
49 delete: Effect.fn(function* () {}),
50});
51
52const state = Layer.effect(
53 State,
54 Effect.sync(() => InMemoryService({})),
55);
56
57const { test, deploy } = Test.make({ providers, state });
58
59const runDeploy = (stack: any) =>
60 Effect.gen(function* () {
61 observed = { ran: false, news: undefined };
62 return yield* Effect.exit(deploy(stack) as Effect.Effect<any>);
63 });
64
65// A bare tag whose `.make(...)` Layer is deliberately never provided.
66class BareWidget extends Widget()("BareWidget") {}
67
68const MissingImplStack = Alchemy.Stack(
69 "PlatformMissingImplStack",
70 { providers, state },
71 Effect.gen(function* () {
72 const widget = yield* yieldWidget(BareWidget);
73 return { name: widget.name };
74 }),
75);
76
77class ProvidedWidget extends Widget()("ProvidedWidget") {}
78const ProvidedWidgetLive = ProvidedWidget.make(
79 { name: "provided" },
80 Effect.succeed({}),
81);
82
83const ProvidedStack = Alchemy.Stack(
84 "PlatformProvidedStack",
85 { providers, state },
86 Effect.gen(function* () {
87 const widget = yield* yieldWidget(ProvidedWidget);
88 return { name: widget.name };
89 }).pipe(Effect.provide([ProvidedWidgetLive as Layer.Layer<never>])),
90);
91
92/** Tag WITH props and no impl — the "external" (non-Effect-native) form. */
93class ExternalWidget extends Widget()("ExternalWidget", {
94 name: "external",
95}) {}
96
97const ExternalStack = Alchemy.Stack(
98 "PlatformExternalStack",
99 { providers, state },
100 Effect.gen(function* () {
101 const widget = yield* yieldWidget(ExternalWidget);
102 return { name: widget.name };
103 }),
104);
105
106const PlainStack = Alchemy.Stack(
107 "PlatformPlainStack",
108 { providers, state },
109 Effect.gen(function* () {
110 const widget = yield* yieldWidget(Widget("PlainWidget", { name: "plain" }));
111 return { name: widget.name };
112 }),
113);
114
115/**
116 * Forward reference: the tag is yielded OUTSIDE the Layer's region — before
117 * the Layer has built — and again inside it. This is the #874 shape (a
118 * worker tag in another worker's `env` resolves during that worker's
119 * async-binding pass) reduced to the minimal harness: the first yield
120 * registers with `undefined` props, the Layer's build repairs them, and the
121 * plan must see the repaired registration.
122 */
123class FwdWidget extends Widget()("FwdWidget") {}
124const FwdWidgetLive = FwdWidget.make({ name: "forward" }, Effect.succeed({}));
125
126const ForwardRefStack = Alchemy.Stack(
127 "PlatformForwardRefStack",
128 { providers, state },
129 Effect.gen(function* () {
130 yield* yieldWidget(FwdWidget);
131 const real = yield* yieldWidget(FwdWidget).pipe(
132 Effect.provide([FwdWidgetLive as Layer.Layer<never>]),
133 );
134 return { name: real.name };
135 }),
136);
137
138/** The bare tag yielded twice — registration must stay idempotent. */
139class DoubleBareWidget extends Widget()("DoubleBareWidget") {}
140
141const DoubleBareStack = Alchemy.Stack(
142 "PlatformDoubleBareStack",
143 { providers, state },
144 Effect.gen(function* () {
145 yield* yieldWidget(DoubleBareWidget);
146 yield* yieldWidget(DoubleBareWidget);
147 return {};
148 }),
149);
150
151describe(
152 "tagged platform resource yielded without its impl layer",
153 { tags: ["local"] },
154 () => {
155 test(
156 "fails fast, naming the class and its layer",
157 Effect.gen(function* () {
158 const exit = yield* runDeploy(MissingImplStack);
159
160 expect(Exit.isFailure(exit)).toBe(true);
161 const message = String(Exit.isFailure(exit) ? exit.cause : "");
162 expect(message).toContain("Test.PlatformWidget<BareWidget>");
163 expect(message).toContain("BareWidget.make(");
164 expect(message).toContain("Provide it to the Stack's program");
165 expect(message).toContain("Effect.provide([BareWidgetLive])");
166 // The provider must never be reached with undefined props.
167 expect(observed.ran).toBe(false);
168 }),
169 { timeout: 60_000 },
170 );
171
172 test(
173 "still deploys when the .make(...) layer IS provided",
174 Effect.gen(function* () {
175 const exit = yield* runDeploy(ProvidedStack);
176 expect(Exit.isSuccess(exit)).toBe(true);
177 expect((observed.news as any)?.name).toBe("provided");
178 }),
179 { timeout: 60_000 },
180 );
181
182 test(
183 "still deploys a tag declared WITH props and no impl",
184 Effect.gen(function* () {
185 const exit = yield* runDeploy(ExternalStack);
186 expect(Exit.isSuccess(exit)).toBe(true);
187 expect((observed.news as any)?.name).toBe("external");
188 expect((observed.news as any)?.isExternal).toBe(true);
189 }),
190 { timeout: 60_000 },
191 );
192
193 test(
194 "still deploys a plain (non-tagged) instance",
195 Effect.gen(function* () {
196 const exit = yield* runDeploy(PlainStack);
197 expect(Exit.isSuccess(exit)).toBe(true);
198 expect((observed.news as any)?.name).toBe("plain");
199 }),
200 { timeout: 60_000 },
201 );
202
203 test(
204 "a forward reference yielded before the layer builds still deploys",
205 Effect.gen(function* () {
206 const exit = yield* runDeploy(ForwardRefStack);
207 expect(Exit.isSuccess(exit)).toBe(true);
208 // The provider must see the Layer's props, not the forward
209 // reference's `undefined`.
210 expect((observed.news as any)?.name).toBe("forward");
211 }),
212 { timeout: 60_000 },
213 );
214
215 test(
216 "yielding the bare tag twice registers once and still fails fast",
217 Effect.gen(function* () {
218 const exit = yield* runDeploy(DoubleBareStack);
219 expect(Exit.isFailure(exit)).toBe(true);
220 const message = String(Exit.isFailure(exit) ? exit.cause : "");
221 expect(message).toContain("Test.PlatformWidget<DoubleBareWidget>");
222 expect(observed.ran).toBe(false);
223 }),
224 { timeout: 60_000 },
225 );
226 },
227);