@@ -3,7 +3,7 @@ import * as NodeFS from "node:fs";
33import * as NodePath from "node:path" ;
44import * as NodeChildProcess from "node:child_process" ;
55import * as NodeServices from "@effect/platform-node/NodeServices" ;
6- import { it } from "@effect/vitest" ;
6+ import { assert , it } from "@effect/vitest" ;
77import * as FileSystem from "effect/FileSystem" ;
88import * as Schema from "effect/Schema" ;
99import * as Duration from "effect/Duration" ;
@@ -13,10 +13,19 @@ import * as TestClock from "effect/testing/TestClock";
1313import { vi } from "vite-plus/test" ;
1414import { HostProcessPlatform } from "@t3tools/shared/hostProcess" ;
1515
16- import { readBootstrapEnvelope } from "./bootstrap.ts" ;
16+ import {
17+ BootstrapEnvelopeDecodeError ,
18+ BootstrapFdStatError ,
19+ BootstrapInputStreamOpenError ,
20+ readBootstrapEnvelope ,
21+ } from "./bootstrap.ts" ;
1722import { assertNone , assertSome } from "@effect/vitest/utils" ;
1823
19- const openSyncInterceptor = vi . hoisted ( ( ) => ( { failPath : null as string | null } ) ) ;
24+ const openSyncInterceptor = vi . hoisted ( ( ) => ( {
25+ failPath : null as string | null ,
26+ errorCode : "ENXIO" ,
27+ } ) ) ;
28+ const fstatSyncInterceptor = vi . hoisted ( ( ) => ( { failFd : null as number | null } ) ) ;
2029
2130vi . mock ( "node:fs" , async ( importOriginal ) => {
2231 const actual = await importOriginal < typeof import ( "node:fs" ) > ( ) ;
@@ -29,12 +38,20 @@ vi.mock("node:fs", async (importOriginal) => {
2938 filePath === openSyncInterceptor . failPath &&
3039 flags === "r"
3140 ) {
32- const error = new Error ( "no such device or address" ) ;
33- Object . assign ( error , { code : "ENXIO" } ) ;
41+ const error = new Error ( `open failed with ${ openSyncInterceptor . errorCode } ` ) ;
42+ Object . assign ( error , { code : openSyncInterceptor . errorCode } ) ;
3443 throw error ;
3544 }
3645 return ( actual . openSync as ( ...a : typeof args ) => number ) ( ...args ) ;
3746 } ,
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+ } ,
3855 } ;
3956} ) ;
4057
@@ -94,6 +111,39 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
94111 } ) ,
95112 ) ;
96113
114+ it . effect ( "preserves fd path, platform, and cause when opening the input stream fails" , ( ) =>
115+ Effect . gen ( function * ( ) {
116+ const fs = yield * FileSystem . FileSystem ;
117+ const filePath = yield * fs . makeTempFileScoped ( { prefix : "t3-bootstrap-" , suffix : ".ndjson" } ) ;
118+ const fd = yield * Effect . acquireRelease (
119+ Effect . sync ( ( ) => NodeFS . openSync ( filePath , "r" ) ) ,
120+ ( fd ) => Effect . sync ( ( ) => NodeFS . closeSync ( fd ) ) ,
121+ ) ;
122+ const fdPath = `/proc/self/fd/${ fd } ` ;
123+
124+ openSyncInterceptor . failPath = fdPath ;
125+ openSyncInterceptor . errorCode = "EIO" ;
126+ try {
127+ const error = yield * readBootstrapEnvelope ( TestEnvelopeSchema , fd , {
128+ timeoutMs : 100 ,
129+ } ) . pipe ( Effect . provideService ( HostProcessPlatform , "linux" ) , Effect . flip ) ;
130+
131+ assert . instanceOf ( error , BootstrapInputStreamOpenError ) ;
132+ assert . equal ( error . fd , fd ) ;
133+ assert . equal ( error . platform , "linux" ) ;
134+ assert . equal ( error . fdPath , fdPath ) ;
135+ assert . equal ( ( error . cause as NodeJS . ErrnoException ) . code , "EIO" ) ;
136+ assert . equal (
137+ error . message ,
138+ `Failed to open bootstrap input stream for file descriptor ${ fd } via '${ fdPath } ' on 'linux'.` ,
139+ ) ;
140+ } finally {
141+ openSyncInterceptor . failPath = null ;
142+ openSyncInterceptor . errorCode = "ENXIO" ;
143+ }
144+ } ) ,
145+ ) ;
146+
97147 it . effect ( "returns none when the fd is unavailable" , ( ) =>
98148 Effect . gen ( function * ( ) {
99149 const fd = NodeFS . openSync ( "/dev/null" , "r" ) ;
@@ -104,6 +154,53 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
104154 } ) ,
105155 ) ;
106156
157+ it . effect ( "preserves fd and cause when stat fails for a non-availability reason" , ( ) =>
158+ Effect . gen ( function * ( ) {
159+ const fd = yield * Effect . acquireRelease (
160+ Effect . sync ( ( ) => NodeFS . openSync ( "/dev/null" , "r" ) ) ,
161+ ( fd ) => Effect . sync ( ( ) => NodeFS . closeSync ( fd ) ) ,
162+ ) ;
163+
164+ fstatSyncInterceptor . failFd = fd ;
165+ try {
166+ const error = yield * readBootstrapEnvelope ( TestEnvelopeSchema , fd , {
167+ timeoutMs : 100 ,
168+ } ) . pipe ( Effect . flip ) ;
169+
170+ assert . instanceOf ( error , BootstrapFdStatError ) ;
171+ assert . equal ( error . fd , fd ) ;
172+ assert . equal ( ( error . cause as NodeJS . ErrnoException ) . code , "EACCES" ) ;
173+ assert . equal ( error . message , `Failed to stat bootstrap file descriptor ${ fd } .` ) ;
174+ } finally {
175+ fstatSyncInterceptor . failFd = null ;
176+ }
177+ } ) ,
178+ ) ;
179+
180+ it . effect ( "preserves fd and schema cause when decoding the envelope fails" , ( ) =>
181+ Effect . gen ( function * ( ) {
182+ const fs = yield * FileSystem . FileSystem ;
183+ const filePath = yield * fs . makeTempFileScoped ( { prefix : "t3-bootstrap-" , suffix : ".ndjson" } ) ;
184+ yield * fs . writeFileString ( filePath , '{"mode":42}\n' ) ;
185+
186+ const fd = yield * Effect . acquireRelease (
187+ Effect . sync ( ( ) => NodeFS . openSync ( filePath , "r" ) ) ,
188+ ( fd ) => Effect . sync ( ( ) => NodeFS . closeSync ( fd ) ) ,
189+ ) ;
190+ const error = yield * readBootstrapEnvelope ( TestEnvelopeSchema , fd , {
191+ timeoutMs : 100 ,
192+ } ) . pipe ( Effect . flip ) ;
193+
194+ assert . instanceOf ( error , BootstrapEnvelopeDecodeError ) ;
195+ assert . equal ( error . fd , fd ) ;
196+ assert . isDefined ( error . cause ) ;
197+ assert . equal (
198+ error . message ,
199+ `Failed to decode bootstrap envelope from file descriptor ${ fd } .` ,
200+ ) ;
201+ } ) ,
202+ ) ;
203+
107204 it . effect ( "returns none when the bootstrap read times out before any value arrives" , ( ) =>
108205 Effect . gen ( function * ( ) {
109206 const fs = yield * FileSystem . FileSystem ;
0 commit comments