diff --git a/.github/workflows/windows-recovery.yml b/.github/workflows/windows-recovery.yml index 91de576062..fd8703f5f6 100644 --- a/.github/workflows/windows-recovery.yml +++ b/.github/workflows/windows-recovery.yml @@ -67,6 +67,21 @@ jobs: exit 1 } + - name: Verify root initialization replacement race + shell: pwsh + run: | + node.exe --test --test-reporter=tap --test-concurrency=1 ` + --test-name-pattern="rejects replacement before opening the temporary marker" ` + packages/storage/dist/__tests__/root-authority.test.js ` + 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP/root-initialization-race.tap" + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { exit $exitCode } + $output = Get-Content "$env:RUNNER_TEMP/root-initialization-race.tap" + if ($output -notcontains '# tests 1' -or $output -notcontains '# pass 1' -or $output -notcontains '# skipped 0') { + Write-Error 'Root initialization race gate did not run exactly one passing Windows test' + exit 1 + } + - name: Verify Runtime Host Local IPC trust boundary shell: pwsh run: | diff --git a/packages/storage/src/__tests__/fixtures/root-initialization-race.ts b/packages/storage/src/__tests__/fixtures/root-initialization-race.ts index 9234c1d756..34c4bdfaa5 100644 --- a/packages/storage/src/__tests__/fixtures/root-initialization-race.ts +++ b/packages/storage/src/__tests__/fixtures/root-initialization-race.ts @@ -18,7 +18,7 @@ */ import fs from 'node:fs'; -import { join } from 'node:path'; +import { basename } from 'node:path'; const [rootArgument, markerFile] = process.argv.slice(2); if (!rootArgument || !markerFile || !process.send) { @@ -26,16 +26,18 @@ if (!rootArgument || !markerFile || !process.send) { } const root = fs.realpathSync(rootArgument); -const markerTempPrefix = join(root, `${markerFile}.`); +const markerTempPrefix = `${markerFile}.`; const originalOpen = fs.promises.open; let intercepted = false; fs.promises.open = (async (path, flags, mode) => { + // This child initializes one root. Match its unique marker basename so the + // cut does not depend on Windows long/short, namespaced, or case spelling. if ( !intercepted && typeof path === 'string' && - path.startsWith(markerTempPrefix) && - path.endsWith('.tmp') + basename(path).startsWith(markerTempPrefix) && + basename(path).endsWith('.tmp') ) { intercepted = true; await send({ type: 'marker_open_pending' }); diff --git a/scripts/ci-test-plan.test.mjs b/scripts/ci-test-plan.test.mjs index e0e9e9c852..8a85a6e490 100644 --- a/scripts/ci-test-plan.test.mjs +++ b/scripts/ci-test-plan.test.mjs @@ -464,6 +464,20 @@ test('Windows recovery executes the exact managed dependency ADS regressions', ( assert.match(recovery, /# skipped 0/u); }); +test('Windows recovery executes the root initialization replacement race', () => { + const recovery = readWorkflow('windows-recovery.yml'); + + assert.match(recovery, /name: Verify root initialization replacement race/u); + assert.match( + recovery, + /--test-name-pattern="rejects replacement before opening the temporary marker"/u, + ); + assert.match(recovery, /packages\/storage\/dist\/__tests__\/root-authority\.test\.js/u); + assert.match(recovery, /# tests 1/u); + assert.match(recovery, /# pass 1/u); + assert.match(recovery, /# skipped 0/u); +}); + test('workflows never persist the job credential into the checkout', () => { for (const name of readdirSync(WORKFLOW_DIR)) { for (const step of checkoutSteps(name)) {