Files
msd-core/tests/npm-integrity-gate.test.cjs
Tom Boucher 6d072435d0 test(#1975): consolidate 51 CLI + scripts-tooling regression tests into module suites
Fold 51 issue-named CLI black-box + scripts-tooling regression files into their
canonical module suites (runtime-launcher-parity, worktree-safety, install-*, managed-hooks,
read-guard, capability-registry, etc.), plus a NEW slash-command-namespace.test.cjs grouping
the 4 slash/colon-namespace-leak invariant suites that had no canonical owner. Verbatim
block-scoped describe wrappers; 427 subtests conserved 1:1.

Host-env pre-check (per B2): no CLI-receiving host sets a redirecting GSD_WORKSTREAM/GSD_PROJECT
value. One folded suite (bug-3668 runtime resolver) creates an extension-less PATH gsd-tools
stub + bash -c; co-locating it with the host's chmodSync tripped local/no-unguarded-nonportable-exec,
so it's now Windows-guarded (skip on win32) matching the host suite's own bash -c guard.

Regenerates regression-name allowlist (222->182), ratchets file-count allowlist (graphify 7->6,
docs entry removed), makes 26 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456;
prunes stale ids). Repoints 13 tests/ references across CONTEXT.md, COMMANDS.md/FEATURES.md
(EN + ja/ko/pt/zh) and ADR-0002. lint:ci green.

Part of epic #1969. Closes #1975.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:22:11 -04:00

290 lines
12 KiB
JavaScript

'use strict';
/**
* Regression test for #114 — npm dependency integrity gate.
*
* Verifies that scripts/check-npm-integrity.cjs correctly detects:
* 1. Clean install — exits 0, no stderr findings
* 2. Version drift — exits 1, stderr names the offending package + both versions
* (reproduces the ws 8.20.1 declared vs 8.20.0 installed incident)
* 3. Extraneous — exits 1 without --ignore-extraneous; exits 0 with it
* 4. Missing — exits 1 regardless of flags
*
* Each fixture lives under tests/fixtures/npm-integrity/<name>/.
* The test spawns the script as a subprocess — no require/import of internals.
*
* Sources:
* - npm CLI docs: https://docs.npmjs.com/cli/v10/commands/npm-ls
* - NIST SSDF PW.4.1: https://csrc.nist.gov/publications/detail/sp/800-218/final
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
const SCRIPT = path.join(ROOT, 'scripts', 'check-npm-integrity.cjs');
const FIXTURES = path.join(__dirname, 'fixtures', 'npm-integrity');
/**
* Run the integrity gate script against a fixture directory.
*
* @param {string} fixtureName - subdirectory under tests/fixtures/npm-integrity/
* @param {string[]} [extraArgs] - additional CLI args passed to the script
* @returns {{ status: number, stdout: string, stderr: string }}
*/
function runGate(fixtureName, extraArgs = []) {
const fixtureDir = path.join(FIXTURES, fixtureName);
const result = spawnSync(process.execPath, [SCRIPT, ...extraArgs], {
cwd: fixtureDir,
encoding: 'utf-8',
timeout: 30_000,
});
return {
status: result.status ?? 1,
stdout: result.stdout ?? '',
stderr: result.stderr ?? '',
};
}
// ─── Scenario 1: Clean ───────────────────────────────────────────────────────
describe('#114: npm integrity gate — clean fixture', () => {
test('exits 0 when install matches lockfile', () => {
const { status } = runGate('clean');
assert.strictEqual(status, 0, 'expected exit 0 for clean install');
});
test('emits no integrity findings to stderr on clean install', () => {
const { stderr } = runGate('clean');
// No "FAIL:" lines expected
assert.ok(
!stderr.includes('FAIL:'),
`expected no FAIL: lines in stderr; got:\n${stderr}`
);
});
});
// ─── Scenario 2: Drift (declared vs installed mismatch) ─────────────────────
// Reproduces: ws 8.20.1 declared in lockfile, 8.20.0 installed in node_modules
// Fixture uses: stable-dep@8.20.1 (declared) vs stable-dep@8.20.0 (installed)
describe('#114: npm integrity gate — drift fixture (declared vs installed mismatch)', () => {
test('exits 1 on version drift', () => {
const { status } = runGate('drift');
assert.strictEqual(status, 1, 'expected exit 1 for version drift');
});
test('stderr names the offending package', () => {
const { stderr } = runGate('drift');
assert.ok(
stderr.includes('stable-dep'),
`expected stderr to name "stable-dep"; got:\n${stderr}`
);
});
test('stderr includes both the declared and installed versions', () => {
const { stderr } = runGate('drift');
assert.ok(
stderr.includes('8.20.0'),
`expected stderr to include installed version "8.20.0"; got:\n${stderr}`
);
assert.ok(
stderr.includes('8.20.1'),
`expected stderr to include declared version "8.20.1"; got:\n${stderr}`
);
});
});
// ─── Scenario 3: Extraneous ──────────────────────────────────────────────────
describe('#114: npm integrity gate — extraneous fixture', () => {
test('exits 1 when extraneous package present (default behavior)', () => {
const { status } = runGate('extraneous');
assert.strictEqual(status, 1, 'expected exit 1 for extraneous package without --ignore-extraneous');
});
test('stderr names the extraneous package', () => {
const { stderr } = runGate('extraneous');
assert.ok(
stderr.includes('ghost-pkg'),
`expected stderr to name "ghost-pkg"; got:\n${stderr}`
);
});
test('exits 0 with --ignore-extraneous flag', () => {
const { status } = runGate('extraneous', ['--ignore-extraneous']);
assert.strictEqual(status, 0, 'expected exit 0 for extraneous package with --ignore-extraneous');
});
});
// ─── Scenario 4: Missing ─────────────────────────────────────────────────────
describe('#114: npm integrity gate — missing fixture', () => {
test('exits 1 when required package is missing from node_modules', () => {
const { status } = runGate('missing');
assert.strictEqual(status, 1, 'expected exit 1 for missing package');
});
test('stderr names the missing package', () => {
const { stderr } = runGate('missing');
assert.ok(
stderr.includes('absent-dep'),
`expected stderr to name "absent-dep"; got:\n${stderr}`
);
});
test('exits 1 even with --ignore-extraneous (missing is not extraneous)', () => {
const { status } = runGate('missing', ['--ignore-extraneous']);
assert.strictEqual(status, 1, 'expected exit 1 for missing package even with --ignore-extraneous');
});
});
// ─── Smoke test: --help ───────────────────────────────────────────────────────
describe('#114: npm integrity gate — --help output', () => {
test('exits 0 with --help flag', () => {
const result = spawnSync(process.execPath, [SCRIPT, '--help'], {
cwd: ROOT,
encoding: 'utf-8',
timeout: 10_000,
});
assert.strictEqual(result.status, 0, '--help should exit 0');
});
test('--help output mentions --ignore-extraneous', () => {
const result = spawnSync(process.execPath, [SCRIPT, '--help'], {
cwd: ROOT,
encoding: 'utf-8',
timeout: 10_000,
});
// The .cjs script writes --help to stdout.
const helpText = (result.stdout ?? '') + (result.stderr ?? '');
assert.ok(
helpText.includes('--ignore-extraneous'),
`expected --help output to document --ignore-extraneous; got:\n${helpText}`
);
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3588-npm-audit-clean.test.cjs — consolidation epic #1969 (B6 #1975)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3588-npm-audit-clean (consolidation epic #1969 B6 #1975)", () => {
'use strict';
/**
* Regression test for #3588 — production dependency tree must not carry
* high or moderate npm-audit advisories.
*
* Strategy: run `npm audit --omit=dev --json` against both the root
* workspace and the embedded SDK package and assert that the metadata
* vulnerability counts are zero across info/low/moderate/high/critical.
*
* The test is intentionally strict — any advisory of any severity (other
* than 'low' if the maintainer accepts it; that branch is left explicit
* here) blocks CI. If a future advisory lands without an upstream patch,
* either bump the patched transitive (preferred), or annotate the
* acceptance below with a justification AND a link to the upstream tracker.
*
* Skips automatically when `node_modules/` is absent (a fresh checkout
* before `npm install`) so the test does not falsely report on developer
* machines mid-setup.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const { execFileSync } = require('node:child_process');
const ROOT = path.resolve(__dirname, '..');
const SDK = path.join(ROOT, 'sdk');
const AUDIT_TIMEOUT_MS = 180_000;
const TEST_TIMEOUT_MS = AUDIT_TIMEOUT_MS + 30_000;
function auditProductionVulns(cwd) {
if (!fs.existsSync(path.join(cwd, 'package.json'))) {
return null; // signal "skip" to caller
}
if (!fs.existsSync(path.join(cwd, 'node_modules'))) {
return null; // signal "skip" to caller
}
const isWindows = process.platform === 'win32';
const npmCandidates = isWindows ? ['npm.cmd', 'npm'] : ['npm'];
const args = ['audit', '--omit=dev', '--json'];
let out;
let lastErr = null;
for (const npmCmd of npmCandidates) {
try {
out = execFileSync(
npmCmd,
args,
{
cwd,
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: AUDIT_TIMEOUT_MS,
shell: isWindows,
}
);
lastErr = null;
break;
} catch (e) {
// `npm audit` exits non-zero when advisories are present; the JSON is
// still on stdout in that case. Recover and let the assertion classify.
if (e && typeof e.stdout !== 'undefined' && e.stdout !== undefined && e.stdout !== null) {
out = Buffer.isBuffer(e.stdout) ? e.stdout.toString('utf-8') : String(e.stdout);
lastErr = null;
break;
}
lastErr = e;
}
}
if (lastErr) throw lastErr;
const parsed = JSON.parse(out);
// `null` is reserved for the "node_modules missing → skip" signal above.
// Any other unexpected JSON shape is a real failure of the audit harness
// (npm changed its output format, audit aborted before metadata, etc.) —
// throw so the test fails loudly instead of skipping silently.
if (parsed && parsed.metadata && parsed.metadata.vulnerabilities) {
return parsed.metadata.vulnerabilities;
}
throw new Error(`Unexpected npm audit JSON shape in ${cwd}: missing metadata.vulnerabilities`);
}
describe('#3588: npm audit --omit=dev reports zero advisories', () => {
test('root workspace production tree has no advisories', { timeout: TEST_TIMEOUT_MS }, (t) => {
const vulns = auditProductionVulns(ROOT);
if (vulns === null) {
t.skip('auditable npm package not present or node_modules/ missing');
return;
}
assert.strictEqual(vulns.critical, 0, `expected 0 critical; got ${vulns.critical}`);
assert.strictEqual(vulns.high, 0, `expected 0 high; got ${vulns.high}`);
assert.strictEqual(vulns.moderate, 0, `expected 0 moderate; got ${vulns.moderate}`);
// Low advisories are not explicitly forbidden by the #3588 acceptance
// criterion but the issue listed only high/moderate as actual findings —
// tighten if any future low advisory is introduced.
assert.strictEqual(vulns.low, 0, `expected 0 low; got ${vulns.low}`);
});
test('sdk/ production tree has no advisories', { timeout: TEST_TIMEOUT_MS }, (t) => {
const vulns = auditProductionVulns(SDK);
if (vulns === null) {
t.skip('sdk/ is not an auditable npm package or sdk/node_modules/ is missing');
return;
}
assert.strictEqual(vulns.critical, 0, `expected 0 critical; got ${vulns.critical}`);
assert.strictEqual(vulns.high, 0, `expected 0 high; got ${vulns.high}`);
assert.strictEqual(vulns.moderate, 0, `expected 0 moderate; got ${vulns.moderate}`);
assert.strictEqual(vulns.low, 0, `expected 0 low; got ${vulns.low}`);
});
});
});
}