1// Test fixture: boots an RpcSpawner, starts the dev sidecar, reconciles a
2// real DevServer through its Command.Dev provider group, prints the
3// dev-server pid, then idles until the test harness kills this parent process.
4// Relative imports (not `@/` alias) so this file runs under both Bun and Node
5// without a paths-aware loader.
6import { newWebSocketRpcSession } from "capnweb";
7import * as Deferred from "effect/Deferred";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Layer from "effect/Layer";
11import * as Schedule from "effect/Schedule";
12import * as FetchHttpClient from "effect/http/FetchHttpClient";
13import * as HttpBody from "effect/http/HttpBody";
14import * as HttpClient from "effect/http/HttpClient";
15import { unwrapRpcHandlers } from "../../../src/Local/RpcSerialization.ts";
16import type { RpcProxyApi } from "../../../src/Local/RpcServer.ts";
17import {
18 encodeSessionEnvironment,
19 SESSION_ENV_PARAM,
20} from "../../../src/Local/RpcServerEnvironment.ts";
21import { layerServer, RpcSpawner } from "../../../src/Local/RpcSpawner.ts";
22import { PlatformServices } from "../../../src/Util/PlatformServices.ts";
23
24const sidecarEntry = process.argv[2];
25const providersUrl = process.argv[3];
26const command = process.argv[4];
27const pidFile = process.argv[5];
28
29if (!sidecarEntry || !providersUrl || !command || !pidFile) {
30 console.error(
31 "usage: rpc-spawner-devserver-parent.ts <sidecar-entry-url> <providers-url> <command> <pid-file>",
32 );
33 process.exit(2);
34}
35
36const program = Effect.gen(function* () {
37 const sp = yield* RpcSpawner;
38 const http = yield* HttpClient.HttpClient;
39 const wsUrl = yield* http
40 .post(sp.url, {
41 body: yield* HttpBody.json({ serverEntryUrl: sidecarEntry }),
42 })
43 .pipe(Effect.flatMap((res) => res.text));
44
45 // Sessions carry the stack environment (children are shared across stacks).
46 const sessionUrl = new URL(wsUrl);
47 sessionUrl.searchParams.set(
48 SESSION_ENV_PARAM,
49 encodeSessionEnvironment({
50 alchemyContext: {
51 dotAlchemy: "/tmp/.alchemy",
52 updateStateStore: false,
53 dev: true,
54 adopt: false,
55 },
56 stack: { name: "test", stage: "dev" },
57 }),
58 );
59 const session = newWebSocketRpcSession<RpcProxyApi>(sessionUrl.toString());
60 const wrapped = yield* Effect.promise(
61 () =>
62 session.getProvider("Command.Dev", providersUrl) as ReturnType<
63 RpcProxyApi["getProvider"]
64 >,
65 );
66 const provider = unwrapRpcHandlers(wrapped, []);
67
68 yield* provider.reconcile({
69 id: "Dev",
70 fqn: "Dev",
71 instanceId: "Dev",
72 news: {
73 command,
74 env: { PID_FILE: pidFile, MARKER: "rpc-devserver" },
75 },
76 olds: undefined,
77 output: undefined,
78 session: {} as never,
79 bindings: [],
80 });
81
82 const pid = yield* Effect.gen(function* () {
83 const fs = yield* FileSystem.FileSystem;
84 const content = yield* fs.readFileString(pidFile);
85 return (JSON.parse(content) as { pid: number }).pid;
86 }).pipe(Effect.retry({ schedule: Schedule.spaced("100 millis"), times: 50 }));
87
88 console.log(`PARENT_PID=${process.pid}\n`);
89 console.log(`DEVSERVER_PID=${pid}\n`);
90
91 const stop = yield* Deferred.make<void>();
92 yield* Deferred.await(stop);
93});
94
95program
96 .pipe(
97 Effect.provide([
98 Layer.provide(
99 layerServer({ profile: undefined, envFile: undefined }),
100 PlatformServices,
101 ),
102 PlatformServices,
103 FetchHttpClient.layer,
104 ]),
105 Effect.scoped,
106 Effect.runPromise,
107 )
108 .catch((e) => {
109 console.error(e);
110 process.exit(1);
111 });