From 1954449085f6ebf3c7bab2787977d6c4aba0befc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:48:51 +0000 Subject: [PATCH 1/2] fix(stability): remove the worktree node_modules link on Linux and macOS too compare-baseline links the main repo's node_modules into each stability worktree with fs.symlinkSync(target, dest, 'junction') and detaches it before removing the worktree with fs.rmdirSync. The 'junction' type only applies on Windows; elsewhere the link is a plain symlink, which rmdirSync rejects with ENOTDIR. On Linux, `npm run preflight:release:quick` therefore crashed in cleanup after both smoke runs, skipped the release threshold check, and left both worktrees behind. removeNodeModulesLink now uses rmdirSync for the Windows junction and unlinkSync elsewhere; both remove only the link, never the main repo's node_modules. main() runs only when the script is executed directly, so the function can be exported and tested. New test: a linked node_modules is removed with its target intact (fails on the old code with the same ENOTDIR), a real node_modules is left alone, and a missing one is a no-op. `npm run test:critical-path:twice` now completes on Linux: 3/3 journeys pass on v5.0.0 and HEAD, thresholds satisfied, both worktrees removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q5Uo5hC58DRSh1qB489Sos --- scripts/stability/compare-baseline.js | 15 ++++- ...pareBaseline.removeNodeModulesLink.test.js | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 test/compareBaseline.removeNodeModulesLink.test.js diff --git a/scripts/stability/compare-baseline.js b/scripts/stability/compare-baseline.js index 45515e6..63cf570 100644 --- a/scripts/stability/compare-baseline.js +++ b/scripts/stability/compare-baseline.js @@ -88,7 +88,14 @@ function removeNodeModulesLink(worktreePath) { return; } if (stats.isSymbolicLink()) { - fs.rmdirSync(linkPath); + // symlinkSync only honours the 'junction' type on Windows. Elsewhere the + // link is a plain symlink, which rmdirSync rejects with ENOTDIR; unlinkSync + // removes the link itself and likewise never touches the target. + if (process.platform === 'win32') { + fs.rmdirSync(linkPath); + } else { + fs.unlinkSync(linkPath); + } } } @@ -222,4 +229,8 @@ function main() { console.log(`\nStability comparison complete: ${outputPath}`); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { removeNodeModulesLink }; diff --git a/test/compareBaseline.removeNodeModulesLink.test.js b/test/compareBaseline.removeNodeModulesLink.test.js new file mode 100644 index 0000000..a6c131b --- /dev/null +++ b/test/compareBaseline.removeNodeModulesLink.test.js @@ -0,0 +1,56 @@ +/** + * @jest-environment node + * + * compare-baseline links the main repo's node_modules into each stability + * worktree and must detach that link before removing the worktree, without + * ever deleting the main repo's real dependencies through it. + */ + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { removeNodeModulesLink } = require('../scripts/stability/compare-baseline'); + +describe('compare-baseline removeNodeModulesLink', () => { + let root; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'stability-link-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + test('removes the linked node_modules and leaves its target intact', () => { + const mainModules = path.join(root, 'main', 'node_modules'); + fs.mkdirSync(path.join(mainModules, 'jest'), { recursive: true }); + fs.writeFileSync(path.join(mainModules, 'jest', 'package.json'), '{}'); + const worktree = path.join(root, 'worktree'); + fs.mkdirSync(worktree); + // Same call as ensureWorktreeDependencies: a junction on Windows, a plain + // symlink elsewhere. + fs.symlinkSync(mainModules, path.join(worktree, 'node_modules'), 'junction'); + + removeNodeModulesLink(worktree); + + expect(fs.existsSync(path.join(worktree, 'node_modules'))).toBe(false); + expect(fs.existsSync(path.join(mainModules, 'jest', 'package.json'))).toBe(true); + }); + + test('leaves a real node_modules directory alone', () => { + const worktree = path.join(root, 'worktree'); + fs.mkdirSync(path.join(worktree, 'node_modules', 'pkg'), { recursive: true }); + + removeNodeModulesLink(worktree); + + expect(fs.existsSync(path.join(worktree, 'node_modules', 'pkg'))).toBe(true); + }); + + test('does nothing when the worktree has no node_modules', () => { + const worktree = path.join(root, 'worktree'); + fs.mkdirSync(worktree); + + expect(() => removeNodeModulesLink(worktree)).not.toThrow(); + }); +}); From 8c5afb1e499e6397a078a63dfdb4ce1a1b2aea68 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 21:24:43 +0000 Subject: [PATCH 2/2] test(stability): run the node_modules link test on the real filesystem The global test setup swaps fs for memfs, which has no Windows junctions, so on the Windows runner the rmdir that removes a real junction failed with ENOTDIR inside memfs. Opt the test out of the fs and os mocks, as the other real-filesystem tests do, so each platform exercises the link it actually creates. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q5Uo5hC58DRSh1qB489Sos --- test/compareBaseline.removeNodeModulesLink.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/compareBaseline.removeNodeModulesLink.test.js b/test/compareBaseline.removeNodeModulesLink.test.js index a6c131b..99366ce 100644 --- a/test/compareBaseline.removeNodeModulesLink.test.js +++ b/test/compareBaseline.removeNodeModulesLink.test.js @@ -4,8 +4,15 @@ * compare-baseline links the main repo's node_modules into each stability * worktree and must detach that link before removing the worktree, without * ever deleting the main repo's real dependencies through it. + * + * Runs on the real filesystem: the global memfs mock has no Windows junctions, + * so it would reject the rmdir that a real junction accepts. */ +jest.unmock('fs'); +jest.unmock('fs/promises'); +jest.unmock('os'); + const fs = require('fs'); const os = require('os'); const path = require('path');