1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeFS from "node:fs";
3import * as NodePath from "node:path";
4import * as NodeChildProcess from "node:child_process";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { assert, it } from "@effect/vitest";
7import * as FileSystem from "effect/FileSystem";
8import * as Schema from "effect/Schema";
9import * as Duration from "effect/Duration";
10import * as Effect from "effect/Effect";
11import * as Fiber from "effect/Fiber";
12import * as TestClock from "effect/testing/TestClock";
13import { vi } from "vite-plus/test";
14import * as HostProcess from "@t3tools/shared/HostProcess";
15
16import {
17 BootstrapEnvelopeDecodeError,
18 BootstrapFdStatError,
19 BootstrapInputStreamOpenError,
20 readBootstrapEnvelope,
21} from "./bootstrap.ts";
22import { assertNone, assertSome } from "@effect/vitest/utils";
23
24const openSyncInterceptor = vi.hoisted(() => ({
25 failPath: null as string | null,
26 errorCode: "ENXIO",
27}));
28const fstatSyncInterceptor = vi.hoisted(() => ({ failFd: null as number | null }));
29
30vi.mock("node:fs", async (importOriginal) => {
31 const actual = await importOriginal<typeof import("node:fs")>();
32 return {
33 ...actual,
34 openSync: (...args: Parameters<typeof actual.openSync>) => {
35 const [filePath, flags] = args;
36 if (
37 typeof filePath === "string" &&
38 filePath === openSyncInterceptor.failPath &&
39 flags === "r"
40 ) {
41 const error = new Error(`open failed with ${openSyncInterceptor.errorCode}`);
42 Object.assign(error, { code: openSyncInterceptor.errorCode });
43 throw error;
44 }
45 return (actual.openSync as (...a: typeof args) => number)(...args);
46 },
47 fstatSync: (...args: Parameters<typeof actual.fstatSync>) => {
48 if (args[0] === fstatSyncInterceptor.failFd) {
49 const error = new Error("permission denied");
50 Object.assign(error, { code: "EACCES" });
51 throw error;
52 }
53 return (actual.fstatSync as (...a: typeof args) => NodeFS.Stats)(...args);
54 },
55 };
56});
57
58const windowsHost = HostProcess.Platform.defaultValue() === "win32";
59const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null";
60const closeIfOpen = (fd: number) => {
61 try {
62 NodeFS.closeSync(fd);
63 } catch (error) {
64 if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error;
65 }
66};
67
68// A successful Windows read streams the inherited fd with autoClose. POSIX
69// reopens the fd through /proc or /dev, so the test still owns the original.
70const openBootstrapInputFd = (filePath: string) =>
71 Effect.acquireRelease(
72 Effect.sync(() => NodeFS.openSync(filePath, "r")),
73 (fd) => (windowsHost ? Effect.void : Effect.sync(() => closeIfOpen(fd))),
74 );
75
76const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
77const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
78
79it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
80 it.effect("reads a bootstrap envelope from a provided fd", () =>
81 Effect.gen(function* () {
82 const fs = yield* FileSystem.FileSystem;
83 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
84
85 yield* fs.writeFileString(
86 filePath,
87 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
88 );
89
90 const fd = yield* openBootstrapInputFd(filePath);
91
92 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
93 assertSome(payload, {
94 mode: "desktop",
95 });
96 }),
97 );
98
99 it.effect("falls back to reading the inherited fd when path duplication fails", () =>
100 Effect.gen(function* () {
101 const fs = yield* FileSystem.FileSystem;
102 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
103
104 yield* fs.writeFileString(
105 filePath,
106 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
107 );
108
109 // Open without acquireRelease: the direct-stream fallback uses autoClose: true,
110 // so the stream owns the fd lifecycle and closes it asynchronously on end.
111 // Attempting to also close it synchronously in a finalizer races with the
112 // stream's async close and produces an uncaught EBADF.
113 const fd = NodeFS.openSync(filePath, "r");
114
115 openSyncInterceptor.failPath = `/proc/self/fd/${fd}`;
116 try {
117 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
118 timeoutMs: 100,
119 }).pipe(Effect.provideService(HostProcess.Platform, "linux"));
120 assertSome(payload, {
121 mode: "desktop",
122 });
123 } finally {
124 openSyncInterceptor.failPath = null;
125 }
126 }),
127 );
128
129 it.effect("preserves fd path, platform, and cause when opening the input stream fails", () =>
130 Effect.gen(function* () {
131 const fs = yield* FileSystem.FileSystem;
132 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
133 const fd = yield* Effect.acquireRelease(
134 Effect.sync(() => NodeFS.openSync(filePath, "r")),
135 (fd) => Effect.sync(() => closeIfOpen(fd)),
136 );
137 const fdPath = `/proc/self/fd/${fd}`;
138
139 openSyncInterceptor.failPath = fdPath;
140 openSyncInterceptor.errorCode = "EIO";
141 try {
142 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
143 timeoutMs: 100,
144 }).pipe(Effect.provideService(HostProcess.Platform, "linux"), Effect.flip);
145
146 assert.instanceOf(error, BootstrapInputStreamOpenError);
147 assert.equal(error.fd, fd);
148 assert.equal(error.platform, "linux");
149 assert.equal(error.fdPath, fdPath);
150 assert.equal((error.cause as NodeJS.ErrnoException).code, "EIO");
151 assert.equal(
152 error.message,
153 `Failed to open bootstrap input stream for file descriptor ${fd} via '${fdPath}' on 'linux'.`,
154 );
155 } finally {
156 openSyncInterceptor.failPath = null;
157 openSyncInterceptor.errorCode = "ENXIO";
158 }
159 }),
160 );
161
162 it.effect("returns none when the fd is unavailable", () =>
163 Effect.gen(function* () {
164 const fd = NodeFS.openSync(nullDevice, "r");
165 NodeFS.closeSync(fd);
166
167 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
168 assertNone(payload);
169 }),
170 );
171
172 it.effect("preserves fd and cause when stat fails for a non-availability reason", () =>
173 Effect.gen(function* () {
174 const fd = yield* Effect.acquireRelease(
175 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),
176 (fd) => Effect.sync(() => closeIfOpen(fd)),
177 );
178
179 fstatSyncInterceptor.failFd = fd;
180 try {
181 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
182 timeoutMs: 100,
183 }).pipe(Effect.flip);
184
185 assert.instanceOf(error, BootstrapFdStatError);
186 assert.equal(error.fd, fd);
187 assert.equal((error.cause as NodeJS.ErrnoException).code, "EACCES");
188 assert.equal(error.message, `Failed to stat bootstrap file descriptor ${fd}.`);
189 } finally {
190 fstatSyncInterceptor.failFd = null;
191 }
192 }),
193 );
194
195 it.effect("preserves fd and schema cause when decoding the envelope fails", () =>
196 Effect.gen(function* () {
197 const fs = yield* FileSystem.FileSystem;
198 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
199 yield* fs.writeFileString(filePath, '{"mode":42}\n');
200
201 const fd = yield* openBootstrapInputFd(filePath);
202 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
203 timeoutMs: 100,
204 }).pipe(Effect.flip);
205
206 assert.instanceOf(error, BootstrapEnvelopeDecodeError);
207 assert.equal(error.fd, fd);
208 assert.isDefined(error.cause);
209 assert.equal(
210 error.message,
211 `Failed to decode bootstrap envelope from file descriptor ${fd}.`,
212 );
213 }),
214 );
215
216 // Needs a FIFO, which mkfifo creates; Windows has neither.
217 it.effect.skipIf(windowsHost)(
218 "returns none when the bootstrap read times out before any value arrives",
219 () =>
220 Effect.gen(function* () {
221 const fs = yield* FileSystem.FileSystem;
222 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });
223 const fifoPath = NodePath.join(tempDir, "bootstrap.pipe");
224
225 yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath]));
226
227 const _writer = yield* Effect.acquireRelease(
228 Effect.sync(() =>
229 NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {
230 stdio: ["ignore", "ignore", "ignore"],
231 }),
232 ),
233 (writer) =>
234 Effect.sync(() => {
235 writer.kill("SIGKILL");
236 }),
237 );
238
239 const fd = yield* Effect.acquireRelease(
240 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
241 (fd) => Effect.sync(() => closeIfOpen(fd)),
242 );
243
244 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
245 timeoutMs: 100,
246 }).pipe(Effect.forkScoped);
247
248 yield* Effect.yieldNow;
249 yield* TestClock.adjust(Duration.millis(100));
250
251 const payload = yield* Fiber.join(fiber);
252 assertNone(payload);
253 }).pipe(Effect.provide(TestClock.layer())),
254 );
255});