diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38f50174318..8e67a7a1c8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -527,3 +527,18 @@ jobs: - name: Validate installed CLI release candidate if: steps.plan.outputs.cli_package == 'true' run: npm run release:cli:smoke -- packages/cli/release/*.tgz + + # Reuse the built output in this runner. These fixtures exercise the + # minimum supported Node's event-loop and experimental SQLite behavior. + - name: Select minimum supported Node + if: steps.plan.outputs.runtime_host == 'true' || contains(steps.plan.outputs.standard_workspaces, 'packages/storage') + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22.19.0' + + - name: Test Node 22 Host and storage lifetimes + if: steps.plan.outputs.runtime_host == 'true' || contains(steps.plan.outputs.standard_workspaces, 'packages/storage') + run: >- + node --test --test-concurrency=1 + packages/runtime-host/dist/__tests__/resumable-peer-stream.test.js + packages/storage/dist/__tests__/managed-dependency-environment-crash.test.js diff --git a/packages/storage/src/__tests__/managed-dependency-environment-crash.test.ts b/packages/storage/src/__tests__/managed-dependency-environment-crash.test.ts index 2d5138e2cb1..3b0e637346a 100644 --- a/packages/storage/src/__tests__/managed-dependency-environment-crash.test.ts +++ b/packages/storage/src/__tests__/managed-dependency-environment-crash.test.ts @@ -172,7 +172,11 @@ function dependencySource() { async function waitForChildReady(child: ChildProcess): Promise { await new Promise((resolve, reject) => { let output = ''; - const timeout = setTimeout(() => finish(new Error('owner child did not become ready')), 15_000); + let diagnostics = ''; + const timeout = setTimeout( + () => finish(new Error(`owner child did not become ready: ${diagnostics}`)), + 15_000, + ); const finish = (error?: Error) => { clearTimeout(timeout); child.stdout?.off('data', onData); @@ -186,9 +190,14 @@ async function waitForChildReady(child: ChildProcess): Promise { output += chunk.toString('utf8'); if (output.includes('READY\n')) finish(); }; - const onErrorData = (chunk: Buffer) => finish(new Error(chunk.toString('utf8'))); + // READY is the startup protocol; stderr may contain Node 22 SQLite warnings. + // Keep bounded diagnostics for exit/timeout failures without rejecting warnings. + const onErrorData = (chunk: Buffer) => { + diagnostics = (diagnostics + chunk.toString('utf8')).slice(-8_192); + }; const onError = (error: Error) => finish(error); - const onExit = (code: number | null) => finish(new Error(`owner child exited early: ${code}`)); + const onExit = (code: number | null) => + finish(new Error(`owner child exited early: ${code}: ${diagnostics}`)); child.stdout?.on('data', onData); child.stderr?.on('data', onErrorData); child.on('error', onError); diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs index 52371951719..b1ad8cd4262 100644 --- a/scripts/ci-workflow-policy.test.mjs +++ b/scripts/ci-workflow-policy.test.mjs @@ -1173,3 +1173,33 @@ function checkoutSteps(name) { [] ); } + +test('the minimum Node verifies Host and storage lifecycle fixtures after building', () => { + const workflow = readWorkflow('ci.yml'); + const start = workflow.indexOf(' - name: Select minimum supported Node'); + assert.ok(start > workflow.indexOf(' - name: Build\n')); + const steps = workflow.slice(start); + const gate = + "if: steps.plan.outputs.runtime_host == 'true' || contains(steps.plan.outputs.standard_workspaces, 'packages/storage')"; + assert.equal(steps.split(gate).length - 1, 2, 'setup and tests share the affected-surface gate'); + const engines = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) + .engines.node; + const lowerBounds = engines.split('||').map((range) => { + const match = /^\s*(?:\^|>=)(\d+\.\d+\.\d+)\s*$/u.exec(range); + assert.ok(match, `unsupported Node engine range: ${range}`); + return match[1]; + }); + lowerBounds.sort((left, right) => { + const a = left.split('.').map(Number); + const b = right.split('.').map(Number); + return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]; + }); + assert.equal(/node-version: '([^']+)'/u.exec(steps)?.[1], lowerBounds[0]); + assert.match(steps, /node --test --test-concurrency=1/u); + assert.match(steps, /packages\/runtime-host\/dist\/__tests__\/resumable-peer-stream\.test\.js/u); + assert.match( + steps, + /packages\/storage\/dist\/__tests__\/managed-dependency-environment-crash\.test\.js/u, + ); + assert.doesNotMatch(steps, /continue-on-error/u); +});