codeburn/app/scripts/stage-cli-packages.test.ts
ozymandiashh 0f5a33450c
Some checks are pending
Tests / test (26) (push) Waiting to run
Tests / test-platforms (macos-latest) (push) Waiting to run
Tests / test-platforms (windows-latest) (push) Waiting to run
Tests / desktop (push) Waiting to run
CI / semgrep (push) Waiting to run
Tests / test (22) (push) Waiting to run
Tests / test (22.13.0) (push) Waiting to run
Tests / test (24) (push) Waiting to run
fix(app): stage the CLI closure without anchoring on the checkout's absolute path (#1473)
npm redacts UUID-shaped path segments to `***` in `npm ls --parseable`, so matching each line against the checkout's absolute <root>/node_modules/ prefix found nothing on any path containing a UUID and packaging aborted with the misleading "is the root npm installed?".
The top-level package name is now read by segment position: after the N-th /node_modules segment, N counted off the real root path, which the redaction renames around but cannot move.
Closure verified identical: 160 packages, same names and versions, same 55M, staged from a UUID worktree and from a UUID-free checkout of main.
Reproduced before and after: main aborts from the UUID path, the branch stages 160 packages from that same path.
A non-zero `npm ls` exit still yields its stdout, so an extraneous package in node_modules does not abort staging.
A missing npm_execpath now fails with its own message instead of degrading into the empty-closure one.
Review patch on the branch: the two hand-rolled path scanners collapse to one split('/node_modules/'), and the npm attribution is corrected from 12 to 11 and later, reproduced on npm 11.19.0.
Fixes #1466.
2026-09-18 04:59:38 -07:00

76 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { topLevelPackagesFromNpmLs } from './stage-cli-packages.mjs'
// `npm ls --parseable` parsed without anchoring on the absolute checkout path.
// npm 11 and later redact UUID-shaped path segments to `***` (observed with
// npm 11.19.0: a checkout at /tmp/1a2b3c4d-1111-2222-3333-444455556666/probe
// prints /private/tmp/***/probe as its root line), which is why #1466's
// absolute-prefix match aborted packaging on any UUID-containing path. These
// fixtures pin the real output shapes, redacted and not. The root's
// node_modules is located by segment position — however many `/node_modules/`
// segments the root path itself contains, the top-level name sits one past
// that count.
const ROOT_MODULES = '/repo/codeburn/node_modules'
describe('topLevelPackagesFromNpmLs', () => {
it('reads top-level names from paths whose UUID segments npm redacted', () => {
const listed = [
'/private/tmp/1a2b3c4d-1111-2222-3333-444455556666/codeburn',
'/private/tmp/1a2b3c4d-1111-2222-3333-444455556666/codeburn/node_modules/@modelcontextprotocol/sdk',
'/private/tmp/1a2b3c4d-1111-2222-3333-444455556666/codeburn/node_modules/chalk',
'/private/tmp/1a2b3c4d-1111-2222-3333-444455556666/codeburn/node_modules/react/node_modules/loose-envify',
].join('\n')
expect(topLevelPackagesFromNpmLs(listed, ROOT_MODULES)).toEqual(new Set(['@modelcontextprotocol/sdk', 'chalk', 'react']))
})
it('keeps the redacted shape working, which the old prefix match could not', () => {
// This is the literal output npm 11.19.0 prints for the checkout above: the
// UUID segment is `***` on every line. The old absolute-prefix match found
// zero packages here and aborted the build.
const listed = [
'/private/tmp/***/probe-29473',
'/private/tmp/***/probe-29473/node_modules/lodash',
].join('\n')
expect(topLevelPackagesFromNpmLs(listed, '/tmp/1a2b3c4d-1111-2222-3333-444455556666/probe-29473/node_modules')).toEqual(new Set(['lodash']))
})
it('stays correct when the checkout itself lives inside a node_modules directory', () => {
// The root path contains one node_modules segment of its own, so the
// project's packages sit one segment deeper. A first-occurrence parser
// would read the checkout's directory name (`codeburn`) off every line.
const nestedRoot = '/other/repo/node_modules/codeburn/node_modules'
const listed = [
'/other/repo/node_modules/codeburn',
'/other/repo/node_modules/codeburn/node_modules/chalk',
'/other/repo/node_modules/codeburn/node_modules/react-dom/node_modules/scheduler',
].join('\n')
expect(topLevelPackagesFromNpmLs(listed, nestedRoot)).toEqual(new Set(['chalk', 'react-dom']))
})
it('maps a nested transitive path to its top-level ancestor, not the leaf', () => {
const listed = '/repo/codeburn/node_modules/react-dom/node_modules/scheduler\n/repo/codeburn/node_modules/react-dom'
expect(topLevelPackagesFromNpmLs(listed, ROOT_MODULES)).toEqual(new Set(['react-dom']))
})
it('normalizes Windows separators on both the lines and the root path', () => {
const listed = [
'C:\\repo\\codeburn',
'C:\\repo\\codeburn\\node_modules\\undici',
'C:\\repo\\codeburn\\node_modules\\@scope\\pkg\\node_modules\\dep',
].join('\n')
expect(topLevelPackagesFromNpmLs(listed, 'C:\\repo\\codeburn\\node_modules')).toEqual(new Set(['undici', '@scope/pkg']))
})
it('skips the project-root line, truncated bare scopes, and empty output', () => {
expect(topLevelPackagesFromNpmLs('/repo/codeburn', ROOT_MODULES)).toEqual(new Set())
expect(topLevelPackagesFromNpmLs('', ROOT_MODULES)).toEqual(new Set())
expect(topLevelPackagesFromNpmLs('\n\n', ROOT_MODULES)).toEqual(new Set())
// A truncated tail that leaves a bare scope is not a package name: without
// the guard, join(root, '@scope') is a real directory and the whole scope
// would be copied as one "package".
expect(topLevelPackagesFromNpmLs('/repo/codeburn/node_modules/@scope', ROOT_MODULES)).toEqual(new Set())
expect(topLevelPackagesFromNpmLs('/repo/codeburn/node_modules/@scope/', ROOT_MODULES)).toEqual(new Set())
})
})