11// @ts -nocheck
22import assert from 'node:assert/strict' ;
3- import { spawn } from 'node:child_process' ;
3+ import { execFile , spawn } from 'node:child_process' ;
4+ import { closeSync , constants , openSync } from 'node:fs' ;
45import { mkdir , mkdtemp , readFile , readdir , rm , writeFile } from 'node:fs/promises' ;
6+ import { Socket } from 'node:net' ;
57import { tmpdir } from 'node:os' ;
68import { join } from 'node:path' ;
79import test from 'node:test' ;
10+ import { promisify } from 'node:util' ;
811import { fileURLToPath } from 'node:url' ;
912
1013import { createIdentityStore } from '../scripts/lib/identity.mjs' ;
@@ -23,6 +26,7 @@ const companionCli = fileURLToPath(new URL('../scripts/zcode-companion.mjs', imp
2326const fakeZCode = fileURLToPath ( new URL ( './fixtures/fake-zcode-cli.mjs' , import . meta. url ) ) ;
2427const cancelAttemptChild = fileURLToPath ( new URL ( './fixtures/cancel-attempt-child.mjs' , import . meta. url ) ) ;
2528const cancelLockHolder = fileURLToPath ( new URL ( './fixtures/cancel-lock-holder.mjs' , import . meta. url ) ) ;
29+ const execFileAsync = promisify ( execFile ) ;
2630
2731function spawnCancelAttempt ( args ) {
2832 const child = spawn ( process . execPath , [ cancelAttemptChild , ...args ] , { stdio : [ 'ignore' , 'pipe' , 'pipe' , 'ipc' ] } ) ; let stdout = '' ; let stderr = '' ;
@@ -33,12 +37,15 @@ function spawnCancelAttempt(args) {
3337
3438function runWriterProbe ( mode ) {
3539 const child = spawn ( process . execPath , [ writerProbe , mode ] , { stdio : [ 'ignore' , 'pipe' , 'pipe' , 'ignore' , 'pipe' ] } ) ;
36- let stdout = '' ; let stderr = '' ; child . stdout . on ( 'data' , ( chunk ) => { stdout += chunk ; } ) ; child . stderr . on ( 'data' , ( chunk ) => { stderr += chunk ; } ) ;
40+ let stdout = '' ; let stderr = '' ; let internalError = null ; let exited = false ; let streamClosed = ! child . stdio [ 4 ] ; let exitCode ;
41+ child . stdout . on ( 'data' , ( chunk ) => { stdout += chunk ; } ) ; child . stderr . on ( 'data' , ( chunk ) => { stderr += chunk ; } ) ;
3742 if ( mode === 'early-close' ) child . stdio [ 4 ] . destroy ( ) ;
3843 if ( mode === 'slow-read' ) { child . stdio [ 4 ] . pause ( ) ; setTimeout ( ( ) => { child . stdio [ 4 ] . on ( 'data' , ( ) => { } ) ; child . stdio [ 4 ] . resume ( ) ; } , 50 ) ; }
3944 return new Promise ( ( resolve , reject ) => {
4045 const timer = setTimeout ( ( ) => { child . kill ( 'SIGKILL' ) ; reject ( new Error ( `writer probe ${ mode } exceeded hard timeout` ) ) ; } , 2_000 ) ;
41- child . once ( 'error' , ( error ) => { clearTimeout ( timer ) ; reject ( error ) ; } ) ; child . once ( 'exit' , ( code ) => { clearTimeout ( timer ) ; resolve ( { code, stdout, stderr } ) ; } ) ;
46+ const settle = ( ) => { if ( ! exited || ! streamClosed ) return ; clearTimeout ( timer ) ; resolve ( { code : exitCode , stdout, stderr, internalError } ) ; } ;
47+ child . stdio [ 4 ] ?. once ( 'error' , ( error ) => { internalError = error ; } ) ; child . stdio [ 4 ] ?. once ( 'close' , ( ) => { streamClosed = true ; settle ( ) ; } ) ;
48+ child . once ( 'error' , ( error ) => { clearTimeout ( timer ) ; reject ( error ) ; } ) ; child . once ( 'exit' , ( code ) => { exitCode = code ; exited = true ; settle ( ) ; } ) ;
4249 } ) ;
4350}
4451
@@ -354,9 +361,31 @@ test('internal response writer cancels a pending write before closing its descri
354361 lateCallback ?. ( null , 1 ) ;
355362} ) ;
356363
364+ test ( 'successful internal response writes unref the protected socket instead of resetting its parent reader' , async ( ) => {
365+ if ( process . platform === 'win32' ) return ;
366+ const root = await mkdtemp ( join ( tmpdir ( ) , 'zcode-fd4-' ) ) ; const fifo = join ( root , 'pipe' ) ;
367+ await execFileAsync ( 'mkfifo' , [ fifo ] ) ;
368+ const readerFd = openSync ( fifo , constants . O_RDONLY | constants . O_NONBLOCK ) ; const writerFd = openSync ( fifo , constants . O_WRONLY | constants . O_NONBLOCK ) ;
369+ const originalDestroy = Socket . prototype . destroy ; const originalUnref = Socket . prototype . unref ;
370+ let destroyed = 0 ; let unrefed = 0 ;
371+ const isWriterSocket = ( socket ) => socket ?. _handle ?. fd === writerFd ;
372+ Socket . prototype . destroy = function ( ...args ) { if ( isWriterSocket ( this ) ) destroyed += 1 ; return originalDestroy . apply ( this , args ) ; } ;
373+ Socket . prototype . unref = function ( ...args ) { if ( isWriterSocket ( this ) ) unrefed += 1 ; return originalUnref . apply ( this , args ) ; } ;
374+ try {
375+ await writeInternalResponse ( { ok : true } , writerFd , { timeoutMs : 100 } ) ;
376+ assert . equal ( destroyed , 0 ) ;
377+ assert . equal ( unrefed , 1 ) ;
378+ } finally {
379+ Socket . prototype . destroy = originalDestroy ; Socket . prototype . unref = originalUnref ;
380+ try { closeSync ( readerFd ) ; } catch { /* writer path may already be closed */ }
381+ try { closeSync ( writerFd ) ; } catch { /* expected while proving success does not close it */ }
382+ await rm ( root , { force : true , recursive : true } ) ;
383+ }
384+ } ) ;
385+
357386test ( 'real fd4 writer is bounded for no-reader, slow-reader, and early-close pipes' , async ( ) => {
358387 const noRead = await runWriterProbe ( 'no-read' ) ; assert . equal ( noRead . code , 0 ) ; assert . match ( noRead . stdout , / I N T E R N A L _ R E S P O N S E _ W R I T E _ T I M E O U T / ) ;
359- const slowRead = await runWriterProbe ( 'slow-read' ) ; assert . equal ( slowRead . code , 0 ) ; assert . match ( slowRead . stdout , / o k / ) ;
388+ const slowRead = await runWriterProbe ( 'slow-read' ) ; assert . equal ( slowRead . code , 0 ) ; assert . match ( slowRead . stdout , / o k / ) ; assert . equal ( slowRead . internalError , null ) ;
360389 const earlyClose = await runWriterProbe ( 'early-close' ) ; assert . equal ( earlyClose . code , 0 ) ; assert . match ( earlyClose . stdout , / I N T E R N A L _ R E S P O N S E _ W R I T E _ F A I L E D / ) ;
361390} ) ;
362391
0 commit comments