| 1 | import { RpcProviderProxy } from "@/Local/RpcProviderProxy"; |
| 2 | import { Stack, type StackSpec } from "@/Stack"; |
| 3 | import * as Core from "@/Test/Core.ts"; |
| 4 | import { expect, it } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Exit from "effect/Exit"; |
| 7 | import * as Layer from "effect/Layer"; |
| 8 | import * 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 | |
| 18 | const options = { |
| 19 | providers: Layer.empty, |
| 20 | dev: true, |
| 21 | }; |
| 22 | |
| 23 | const COMMAND_LOCAL = import.meta.resolve( |
| 24 | "../../src/Command/Local.ts", |
| 25 | import.meta.url, |
| 26 | ); |
| 27 | |
| 28 | type StackShape = Omit<StackSpec, "output">; |
| 29 | |
| 30 | const dummyStack = (name: string): StackShape => ({ |
| 31 | name, |
| 32 | stage: "test", |
| 33 | resources: {}, |
| 34 | bindings: {}, |
| 35 | actions: {}, |
| 36 | }); |
| 37 | |
| 38 | const 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 | |
| 54 | it.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 | ); |