Skip to content

Commit 6c84c32

Browse files
indexzeroclaude
andauthored
fix(test) resolve cyclonedx binary once instead of using npx per call (#18)
* fix(test) resolve cyclonedx binary once instead of using npx per call Concurrent npx invocations race on the shared npx cache, causing ENOTEMPTY on directory renames. Instead, resolve the cyclonedx-npm binary once (via require.resolve or a one-time npm install into a temp prefix) and invoke it directly with node for all subsequent calls. * fix(test) align test:coverage glob with test glob test:coverage was missing ./test/**/*.test.js, so verification and parser-specific tests were excluded from coverage runs. * fix(test) decode fixtures before tests, add parsers to CI coverage Add pretest/pretest:coverage hooks to run fixture decoding so that test/parsers/*.test.js can find decoded fixtures in CI. Align test and test:coverage globs to include test/parsers/*.test.js. Add test:all for running the full suite including verification and monorepo tests locally. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ca60ae1 commit 6c84c32

2 files changed

Lines changed: 50 additions & 12 deletions

File tree

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@
4747
"bin/flatcover.js"
4848
],
4949
"scripts": {
50-
"test": "node --test ./test/*.test.js ./test/**/*.test.js",
51-
"test:coverage": "c8 node --test ./test/*.test.js",
50+
"pretest": "node test/fixtures/decode.js",
51+
"pretest:coverage": "node test/fixtures/decode.js",
52+
"test": "node --test ./test/*.test.js ./test/parsers/*.test.js",
53+
"test:coverage": "c8 node --test ./test/*.test.js ./test/parsers/*.test.js",
54+
"test:all": "node --test ./test/*.test.js ./test/**/*.test.js",
5255
"build:types": "tsc",
5356
"lint": "biome lint src test",
5457
"lint:fix": "biome lint --write src test",

test/support/parity.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,61 @@ async function setupAndInstall(packageName, version) {
9898
return tmpDir;
9999
}
100100

101+
/**
102+
* Resolve the cyclonedx-npm binary once, installing if needed.
103+
* Avoids repeated npx invocations that race on the shared npx cache.
104+
* @returns {Promise<string>} Absolute path to cyclonedx-npm-cli.js
105+
*/
106+
let _cyclonedxBin;
107+
async function getCycloneDXBin() {
108+
if (_cyclonedxBin) return _cyclonedxBin;
109+
110+
// Try to resolve from node_modules first (e.g. if installed as devDep)
111+
try {
112+
const result = await x('node', [
113+
'-e',
114+
'console.log(require.resolve("@cyclonedx/cyclonedx-npm/bin/cyclonedx-npm-cli.js"))'
115+
]);
116+
if (result.exitCode === 0 && result.stdout.trim()) {
117+
_cyclonedxBin = result.stdout.trim();
118+
return _cyclonedxBin;
119+
}
120+
} catch {
121+
// fall through to npx install
122+
}
123+
124+
// Install once into a temp prefix and resolve the binary
125+
const prefix = join(tmpdir(), 'flatlock-cyclonedx');
126+
const install = await x('npm', ['install', '--prefix', prefix, '@cyclonedx/cyclonedx-npm']);
127+
if (install.exitCode !== 0) {
128+
throw new Error(`Failed to install @cyclonedx/cyclonedx-npm: ${install.stderr}`);
129+
}
130+
_cyclonedxBin = join(
131+
prefix,
132+
'node_modules',
133+
'@cyclonedx',
134+
'cyclonedx-npm',
135+
'bin',
136+
'cyclonedx-npm-cli.js'
137+
);
138+
return _cyclonedxBin;
139+
}
140+
101141
/**
102142
* Run CycloneDX on a directory
103143
* @param {string} dir
104144
* @param {{ lockfileOnly?: boolean }} options
105145
* @returns {Promise<Set<string>>} Set of name@version strings
106146
*/
107147
async function runCycloneDX(dir, { lockfileOnly = false } = {}) {
108-
const args = [
109-
'@cyclonedx/cyclonedx-npm',
110-
'--output-format',
111-
'JSON',
112-
'--flatten-components',
113-
'--omit',
114-
'dev'
115-
];
148+
const bin = await getCycloneDXBin();
149+
const args = [bin, '--output-format', 'JSON', '--flatten-components', '--omit', 'dev'];
116150

117151
if (lockfileOnly) {
118152
args.push('--package-lock-only');
119153
}
120154

121-
const result = await x('npx', args, {
155+
const result = await x('node', args, {
122156
nodeOptions: { cwd: dir }
123157
});
124158

@@ -217,7 +251,8 @@ export async function getParityResults(packageName, version) {
217251
const tmpDir = await setupAndInstall(packageName, version);
218252

219253
try {
220-
// Run both on the same lockfile
254+
// Run all three in parallel — safe now that CycloneDX uses a
255+
// pre-resolved binary instead of npx.
221256
const [cyclonedxLockfile, cyclonedxNodeModules, flatlock] = await Promise.all([
222257
runCycloneDX(tmpDir, { lockfileOnly: true }),
223258
runCycloneDX(tmpDir, { lockfileOnly: false }),

0 commit comments

Comments
 (0)