From 75ddda504174adaeef76daaf9663ba8cdbf048be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Oct 2025 09:44:35 +0000 Subject: [PATCH 1/2] feat: Add waitForFile to handle CI race conditions Co-authored-by: charles.francoise --- packages/ssh/src/server/server.e2e.test.ts | 32 +++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/ssh/src/server/server.e2e.test.ts b/packages/ssh/src/server/server.e2e.test.ts index 08a7c7f..b5ea7b2 100644 --- a/packages/ssh/src/server/server.e2e.test.ts +++ b/packages/ssh/src/server/server.e2e.test.ts @@ -9,6 +9,7 @@ */ import { execFile } from 'child_process'; +import { access } from 'fs/promises'; import { promisify } from 'util'; import { build } from 'tsdown'; @@ -26,6 +27,32 @@ import { serverTarget } from '../../tsdown.config'; const execFileAsync = promisify(execFile); +/** + * Wait for a file to be fully written and accessible. + * This helps prevent race conditions where the build completes + * but the file hasn't been fully flushed to disk yet. + */ +async function waitForFile( + filePath: string, + maxAttempts = 10, + delayMs = 100, +): Promise { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + await access(filePath); + // File exists, now try to verify it's readable by executing it with --help + // This ensures it's not just present but also syntactically valid + await execFileAsync('node', [filePath, '--help'], { timeout: 2000 }); + return; + } catch { + if (attempt < maxAttempts - 1) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + } + throw new Error(`File ${filePath} not ready after ${maxAttempts} attempts`); +} + /** * Helper to run the server with args and capture output. * The spawned process inherits the current process.env. @@ -77,7 +104,10 @@ describe('MCP Server Executable - End-to-End Tests', () => { // Build the project await build({ ...serverTarget, logLevel: 'silent' }); - // Use the built executable + // Wait for the file to be fully written and accessible + // This prevents race conditions in CI where the file might not be + // fully flushed to disk immediately after the build completes + await waitForFile('dist/server.js'); } catch (error) { const execError = error as { stdout?: string; stderr?: string }; const errorMessage = [ From aeae1bc30ed502a64e8cae7dad420e5b7ba0587c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 28 Oct 2025 09:49:19 +0000 Subject: [PATCH 2/2] Refactor: Simplify waitForFile function signature Co-authored-by: charles.francoise --- packages/ssh/src/server/server.e2e.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/ssh/src/server/server.e2e.test.ts b/packages/ssh/src/server/server.e2e.test.ts index b5ea7b2..71b120d 100644 --- a/packages/ssh/src/server/server.e2e.test.ts +++ b/packages/ssh/src/server/server.e2e.test.ts @@ -32,11 +32,10 @@ const execFileAsync = promisify(execFile); * This helps prevent race conditions where the build completes * but the file hasn't been fully flushed to disk yet. */ -async function waitForFile( - filePath: string, - maxAttempts = 10, - delayMs = 100, -): Promise { +async function waitForFile(filePath: string): Promise { + const maxAttempts = 10; + const delayMs = 100; + for (let attempt = 0; attempt < maxAttempts; attempt++) { try { await access(filePath);