From eed77312e91e217bd8f1ffa701e819d3970bd424 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Fri, 19 Jun 2026 09:58:11 +0200 Subject: [PATCH 1/4] Fix flaky daemon log test on Windows by polling for flushed output --- apps/cli/tests/daemon.test.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/cli/tests/daemon.test.ts b/apps/cli/tests/daemon.test.ts index 06dc61e125..974b65c4f6 100644 --- a/apps/cli/tests/daemon.test.ts +++ b/apps/cli/tests/daemon.test.ts @@ -9,6 +9,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const testProcessName = 'studio-site-process-manager-test'; const tmpDir = path.join( os.tmpdir(), 'studio-daemon-test' ); +// The daemon pipes child output through readline into a buffered WriteStream, so the +// bytes reach disk several async hops after we write them. Poll instead of relying on a +// fixed sleep, which races on slower agents (notably Windows CI). +async function waitForFileToContain( + filePath: string, + expected: string, + timeoutMs = 2000 +): Promise< string > { + const start = Date.now(); + let contents = ''; + do { + try { + contents = fs.readFileSync( filePath, 'utf8' ); + } catch { + contents = ''; + } + if ( contents.includes( expected ) ) { + return contents; + } + await new Promise( ( resolve ) => setTimeout( resolve, 10 ) ); + } while ( Date.now() - start < timeoutMs ); + return contents; +} + class MockChildProcess extends EventEmitter { pid = 4321; connected = true; @@ -124,15 +148,15 @@ describe( 'ProcessManagerDaemon', () => { '0' ) }${ String( now.getDate() ).padStart( 2, '0' ) }`; expect( - fs.readFileSync( + await waitForFileToContain( path.join( tmpDir, 'logs', `${ testProcessName }-out-${ dateTag }.log` ), - 'utf8' + 'fixture-stdout' ) ).toContain( 'fixture-stdout' ); expect( - fs.readFileSync( + await waitForFileToContain( path.join( tmpDir, 'logs', `${ testProcessName }-error-${ dateTag }.log` ), - 'utf8' + 'fixture-stderr' ) ).toContain( 'fixture-stderr' ); } ); From c8aba10aabf92819966dd63f6f8ca2cfe9ff0e2c Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Fri, 19 Jun 2026 10:01:52 +0200 Subject: [PATCH 2/4] Retry the log assertion directly instead of double-checking content --- apps/cli/tests/daemon.test.ts | 59 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/apps/cli/tests/daemon.test.ts b/apps/cli/tests/daemon.test.ts index 974b65c4f6..78b2dcd377 100644 --- a/apps/cli/tests/daemon.test.ts +++ b/apps/cli/tests/daemon.test.ts @@ -9,28 +9,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const testProcessName = 'studio-site-process-manager-test'; const tmpDir = path.join( os.tmpdir(), 'studio-daemon-test' ); -// The daemon pipes child output through readline into a buffered WriteStream, so the -// bytes reach disk several async hops after we write them. Poll instead of relying on a -// fixed sleep, which races on slower agents (notably Windows CI). -async function waitForFileToContain( - filePath: string, - expected: string, - timeoutMs = 2000 -): Promise< string > { +// Retries an assertion until it passes or the timeout elapses, rethrowing the last +// failure. The daemon pipes child output through readline into a buffered WriteStream, so +// the bytes reach disk several async hops after we write them — a fixed sleep races on +// slower agents (notably Windows CI). +async function waitFor( assertion: () => void, timeoutMs = 2000 ): Promise< void > { const start = Date.now(); - let contents = ''; - do { + for (;;) { try { - contents = fs.readFileSync( filePath, 'utf8' ); - } catch { - contents = ''; - } - if ( contents.includes( expected ) ) { - return contents; + assertion(); + return; + } catch ( error ) { + if ( Date.now() - start >= timeoutMs ) { + throw error; + } + await new Promise( ( resolve ) => setTimeout( resolve, 10 ) ); } - await new Promise( ( resolve ) => setTimeout( resolve, 10 ) ); - } while ( Date.now() - start < timeoutMs ); - return contents; + } } class MockChildProcess extends EventEmitter { @@ -147,18 +142,20 @@ describe( 'ProcessManagerDaemon', () => { 2, '0' ) }${ String( now.getDate() ).padStart( 2, '0' ) }`; - expect( - await waitForFileToContain( - path.join( tmpDir, 'logs', `${ testProcessName }-out-${ dateTag }.log` ), - 'fixture-stdout' - ) - ).toContain( 'fixture-stdout' ); - expect( - await waitForFileToContain( - path.join( tmpDir, 'logs', `${ testProcessName }-error-${ dateTag }.log` ), - 'fixture-stderr' - ) - ).toContain( 'fixture-stderr' ); + await waitFor( () => { + expect( + fs.readFileSync( + path.join( tmpDir, 'logs', `${ testProcessName }-out-${ dateTag }.log` ), + 'utf8' + ) + ).toContain( 'fixture-stdout' ); + expect( + fs.readFileSync( + path.join( tmpDir, 'logs', `${ testProcessName }-error-${ dateTag }.log` ), + 'utf8' + ) + ).toContain( 'fixture-stderr' ); + } ); } ); it( 'includes captured stderr in the exit event payload', async () => { From ef7b020d9bf2be75efc3b8135940659c021aa632 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Fri, 19 Jun 2026 10:06:26 +0200 Subject: [PATCH 3/4] Use Vitest's built-in vi.waitFor instead of a hand-rolled poller --- apps/cli/tests/daemon.test.ts | 53 +++++++++++++---------------------- 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/apps/cli/tests/daemon.test.ts b/apps/cli/tests/daemon.test.ts index 78b2dcd377..ae3eade109 100644 --- a/apps/cli/tests/daemon.test.ts +++ b/apps/cli/tests/daemon.test.ts @@ -9,25 +9,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const testProcessName = 'studio-site-process-manager-test'; const tmpDir = path.join( os.tmpdir(), 'studio-daemon-test' ); -// Retries an assertion until it passes or the timeout elapses, rethrowing the last -// failure. The daemon pipes child output through readline into a buffered WriteStream, so -// the bytes reach disk several async hops after we write them — a fixed sleep races on -// slower agents (notably Windows CI). -async function waitFor( assertion: () => void, timeoutMs = 2000 ): Promise< void > { - const start = Date.now(); - for (;;) { - try { - assertion(); - return; - } catch ( error ) { - if ( Date.now() - start >= timeoutMs ) { - throw error; - } - await new Promise( ( resolve ) => setTimeout( resolve, 10 ) ); - } - } -} - class MockChildProcess extends EventEmitter { pid = 4321; connected = true; @@ -142,20 +123,26 @@ describe( 'ProcessManagerDaemon', () => { 2, '0' ) }${ String( now.getDate() ).padStart( 2, '0' ) }`; - await waitFor( () => { - expect( - fs.readFileSync( - path.join( tmpDir, 'logs', `${ testProcessName }-out-${ dateTag }.log` ), - 'utf8' - ) - ).toContain( 'fixture-stdout' ); - expect( - fs.readFileSync( - path.join( tmpDir, 'logs', `${ testProcessName }-error-${ dateTag }.log` ), - 'utf8' - ) - ).toContain( 'fixture-stderr' ); - } ); + // The daemon pipes child output through readline into a buffered WriteStream, so the + // bytes reach disk several async hops after we write them. Poll the logs instead of + // relying on a fixed sleep, which races on slower agents (notably Windows CI). + await vi.waitFor( + () => { + expect( + fs.readFileSync( + path.join( tmpDir, 'logs', `${ testProcessName }-out-${ dateTag }.log` ), + 'utf8' + ) + ).toContain( 'fixture-stdout' ); + expect( + fs.readFileSync( + path.join( tmpDir, 'logs', `${ testProcessName }-error-${ dateTag }.log` ), + 'utf8' + ) + ).toContain( 'fixture-stderr' ); + }, + { timeout: 2000 } + ); } ); it( 'includes captured stderr in the exit event payload', async () => { From d3b75dc3b830b77e1f4693838a4add9c2a8cffc5 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Fri, 19 Jun 2026 10:08:03 +0200 Subject: [PATCH 4/4] Reword log-poll comment to explain the current code --- apps/cli/tests/daemon.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/tests/daemon.test.ts b/apps/cli/tests/daemon.test.ts index ae3eade109..b42e1fe44c 100644 --- a/apps/cli/tests/daemon.test.ts +++ b/apps/cli/tests/daemon.test.ts @@ -124,8 +124,8 @@ describe( 'ProcessManagerDaemon', () => { '0' ) }${ String( now.getDate() ).padStart( 2, '0' ) }`; // The daemon pipes child output through readline into a buffered WriteStream, so the - // bytes reach disk several async hops after we write them. Poll the logs instead of - // relying on a fixed sleep, which races on slower agents (notably Windows CI). + // bytes reach disk several async hops after we write them. Poll until the output + // lands; the flush can lag on slower agents (notably Windows CI). await vi.waitFor( () => { expect(