1import { RpcProviderProxy } from "@/Local/RpcProviderProxy";
2import { Stack, type StackSpec } from "@/Stack";
3import * as Core from "@/Test/Core.ts";
4import { expect, it } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import * as Scope from "effect/Scope";
9
10/**
11 * The process-wide sidecar HTTP server must outlive the test file whose
12 * `get()` first built it. `toEffect` runs each file in that file's
13 * `sharedScope`; if that Scope leaks into the spawner layer, the file's
14 * afterAll reloads the Bun.serve fetch handler away and later files POST
15 * "Welcome to Bun!" instead of a websocket URL.
16 */
17
18const options = {
19 providers: Layer.empty,
20 dev: true,
21};
22
23const COMMAND_LOCAL = import.meta.resolve(
24 "../../src/Command/Local.ts",
25 import.meta.url,
26);
27
28type StackShape = Omit<StackSpec, "output">;
29
30const dummyStack = (name: string): StackShape => ({
31 name,
32 stage: "test",
33 resources: {},
34 bindings: {},
35 actions: {},
36});
37
38const runAsFile = <A, E, R>(
39 handle: NonNullable<ReturnType<typeof Core.makeSidecarHandle>>,
40 stackName: string,
41 body: Effect.Effect<A, E, R>,
42) => {
43 const sharedScope = Scope.makeUnsafe("sequential");
44 return Core.toEffect(
45 body.pipe(
46 Effect.provideService(Stack, dummyStack(stackName)),
47 ) as Core.TestEffect<A>,
48 options,
49 sharedScope,
50 handle,
51 ).pipe(Effect.ensuring(Scope.close(sharedScope, Exit.void)));
52};
53
54it.live(
55 "sidecar POST still returns a ws URL after the first file's scope closes",
56 () =>
57 Effect.gen(function* () {
58 const handle = Core.makeSidecarHandle(options);
59 expect(handle).toBeDefined();
60 if (handle === undefined) return;
61
62 const spawn = (stackName: string) =>
63 runAsFile(
64 handle,
65 stackName,
66 Effect.gen(function* () {
67 const proxy = yield* RpcProviderProxy;
68 const provider = yield* proxy.get(COMMAND_LOCAL, "Command.Dev");
69 expect(provider).toBeDefined();
70 }),
71 );
72
73 yield* spawn("sidecar-lifetime-a");
74 yield* spawn("sidecar-lifetime-b");
75 yield* handle.close;
76 }),
77 { tags: ["local"], timeout: 60_000 },
78);