Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
503 lines
20 KiB
JavaScript
503 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Unit tests for scripts/npm-audit-baseline.cjs (#4196).
|
|
*
|
|
* Covers the pure diff/verdict functions directly, plus the git-object-level
|
|
* extraction and env-driven ref resolution using real throwaway git fixture
|
|
* repos (no network, no real npm registry round-trip -- runPackageLockAudit
|
|
* is only exercised here for its filesystem-only skip conditions).
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const {
|
|
AUDIT_DIFF_REASON,
|
|
diffNewVulnerablePackages,
|
|
evaluateAuditDiff,
|
|
runPackageLockAudit,
|
|
runInstalledTreeAudit,
|
|
runNpmAuditWithRetry,
|
|
extractBaselineTree,
|
|
resolveBaselineRef,
|
|
isTimeoutKill,
|
|
buildTimeoutKillError,
|
|
AUDIT_BACKOFF_BASE_MS,
|
|
NULL_SHA,
|
|
} = require('../scripts/npm-audit-baseline.cjs');
|
|
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
const GIT_TIMEOUT_MS = 30_000;
|
|
|
|
function git(args, cwd) {
|
|
return execFileSync('git', args, {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
timeout: GIT_TIMEOUT_MS,
|
|
}).trim();
|
|
}
|
|
|
|
/**
|
|
* Builds a throwaway git repo with one commit containing package.json +
|
|
* package-lock.json (and optionally under a subdir), returning
|
|
* { dir, commitSha }.
|
|
*/
|
|
function makeCommittedFixtureRepo(t, { subdir = '', pkgContent = '{"name":"fixture"}', lockContent = '{"lockfileVersion":3}' } = {}) {
|
|
const dir = createTempDir('msd-audit-baseline-fixture-');
|
|
t.after(() => cleanup(dir));
|
|
git(['init', '-q'], dir);
|
|
git(['config', 'user.email', 'test@example.com'], dir);
|
|
git(['config', 'user.name', 'Test'], dir);
|
|
const targetDir = subdir ? path.join(dir, subdir) : dir;
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
fs.writeFileSync(path.join(targetDir, 'package.json'), pkgContent);
|
|
fs.writeFileSync(path.join(targetDir, 'package-lock.json'), lockContent);
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'fixture commit'], dir);
|
|
const commitSha = git(['rev-parse', 'HEAD'], dir);
|
|
return { dir, commitSha };
|
|
}
|
|
|
|
// ─── diffNewVulnerablePackages ────────────────────────────────────────────
|
|
|
|
describe('diffNewVulnerablePackages', () => {
|
|
test('empty baseline + empty head -> []', () => {
|
|
assert.deepStrictEqual(diffNewVulnerablePackages({}, {}), []);
|
|
});
|
|
|
|
test('baseline and head share the same packages -> [] (nothing new)', () => {
|
|
const baseline = { a: {}, b: {} };
|
|
const head = { a: {}, b: {} };
|
|
assert.deepStrictEqual(diffNewVulnerablePackages(baseline, head), []);
|
|
});
|
|
|
|
test('head adds a package not in baseline -> only the addition', () => {
|
|
const baseline = { a: {} };
|
|
const head = { a: {}, b: {} };
|
|
assert.deepStrictEqual(diffNewVulnerablePackages(baseline, head), ['b']);
|
|
});
|
|
|
|
test('empty baseline, head has one package -> that package', () => {
|
|
assert.deepStrictEqual(diffNewVulnerablePackages({}, { a: {} }), ['a']);
|
|
});
|
|
|
|
test('packages removed from head (present in baseline only) are not "new"', () => {
|
|
const baseline = { a: {}, b: {}, c: {} };
|
|
const head = { a: {} };
|
|
assert.deepStrictEqual(diffNewVulnerablePackages(baseline, head), []);
|
|
});
|
|
|
|
test('baseline and head both undefined -> [] (must not throw)', () => {
|
|
assert.deepStrictEqual(diffNewVulnerablePackages(undefined, undefined), []);
|
|
});
|
|
});
|
|
|
|
// ─── evaluateAuditDiff ─────────────────────────────────────────────────────
|
|
|
|
describe('evaluateAuditDiff', () => {
|
|
test('no new packages -> ok:true with OK_NO_NEW_VULNERABILITIES and preExisting list', () => {
|
|
const baselineVulnerabilities = { a: {} };
|
|
const headVulnerabilities = { a: {} };
|
|
const result = evaluateAuditDiff({ baselineVulnerabilities, headVulnerabilities });
|
|
assert.deepStrictEqual(result, {
|
|
ok: true,
|
|
reason: AUDIT_DIFF_REASON.OK_NO_NEW_VULNERABILITIES,
|
|
preExisting: ['a'],
|
|
});
|
|
});
|
|
|
|
test('one new package -> ok:false with FAIL_NEW_VULNERABLE_PACKAGE and exact newlyIntroduced', () => {
|
|
const baselineVulnerabilities = { a: {} };
|
|
const headVulnerabilities = { a: {}, b: {} };
|
|
const result = evaluateAuditDiff({ baselineVulnerabilities, headVulnerabilities });
|
|
assert.strictEqual(result.ok, false);
|
|
assert.strictEqual(result.reason, AUDIT_DIFF_REASON.FAIL_NEW_VULNERABLE_PACKAGE);
|
|
assert.deepStrictEqual(result.newlyIntroduced, ['b']);
|
|
});
|
|
|
|
test('multiple new packages -> all listed', () => {
|
|
const baselineVulnerabilities = {};
|
|
const headVulnerabilities = { a: {}, b: {}, c: {} };
|
|
const result = evaluateAuditDiff({ baselineVulnerabilities, headVulnerabilities });
|
|
assert.strictEqual(result.ok, false);
|
|
assert.deepStrictEqual(result.newlyIntroduced.sort(), ['a', 'b', 'c']);
|
|
});
|
|
});
|
|
|
|
// ─── resolveBaselineRef ─────────────────────────────────────────────────────
|
|
|
|
describe('resolveBaselineRef', () => {
|
|
const envKeys = ['AUDIT_BASELINE_REF', 'GITHUB_BASE_REF', 'GITHUB_EVENT_NAME'];
|
|
let originalEnv;
|
|
|
|
beforeEach(() => {
|
|
originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
|
for (const key of envKeys) delete process.env[key];
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const key of envKeys) {
|
|
if (originalEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = originalEnv[key];
|
|
}
|
|
});
|
|
|
|
test('AUDIT_BASELINE_REF set -> returned verbatim, highest priority even with others set', (t) => {
|
|
const { dir } = makeCommittedFixtureRepo(t);
|
|
process.env.AUDIT_BASELINE_REF = 'some/explicit-ref';
|
|
process.env.GITHUB_BASE_REF = 'next';
|
|
process.env.GITHUB_EVENT_NAME = 'push';
|
|
assert.strictEqual(resolveBaselineRef(dir), 'some/explicit-ref');
|
|
});
|
|
|
|
test('AUDIT_BASELINE_REF unset, GITHUB_BASE_REF=next -> origin/next', (t) => {
|
|
const { dir } = makeCommittedFixtureRepo(t);
|
|
process.env.GITHUB_BASE_REF = 'next';
|
|
assert.strictEqual(resolveBaselineRef(dir), 'origin/next');
|
|
});
|
|
|
|
test('neither set, push event, HEAD~1 resolves -> returns that parent sha', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-fixture-');
|
|
t.after(() => cleanup(dir));
|
|
git(['init', '-q'], dir);
|
|
git(['config', 'user.email', 'test@example.com'], dir);
|
|
git(['config', 'user.name', 'Test'], dir);
|
|
fs.writeFileSync(path.join(dir, 'a.txt'), 'first');
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'first commit'], dir);
|
|
fs.writeFileSync(path.join(dir, 'a.txt'), 'second');
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'second commit'], dir);
|
|
|
|
// Compute expected parent sha independently, not via resolveBaselineRef.
|
|
const expectedParentSha = git(['rev-parse', 'HEAD~1'], dir);
|
|
|
|
process.env.GITHUB_EVENT_NAME = 'push';
|
|
assert.strictEqual(resolveBaselineRef(dir), expectedParentSha);
|
|
});
|
|
|
|
test('NULL_SHA is the documented all-zeros 40-char sentinel', () => {
|
|
assert.strictEqual(NULL_SHA, '0'.repeat(40));
|
|
assert.strictEqual(NULL_SHA.length, 40);
|
|
});
|
|
|
|
test('push event but only one commit (HEAD~1 does not exist) falls through without choking', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-fixture-');
|
|
t.after(() => cleanup(dir));
|
|
git(['init', '-q'], dir);
|
|
git(['config', 'user.email', 'test@example.com'], dir);
|
|
git(['config', 'user.name', 'Test'], dir);
|
|
fs.writeFileSync(path.join(dir, 'a.txt'), 'only');
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'only commit'], dir);
|
|
|
|
process.env.GITHUB_EVENT_NAME = 'push';
|
|
// No origin/next remote-tracking ref exists in this throwaway repo, so
|
|
// this must fall all the way through to ''.
|
|
assert.strictEqual(resolveBaselineRef(dir), '');
|
|
});
|
|
|
|
test('no origin/next, but a plain local branch named next exists -> returns "next"', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-fixture-');
|
|
t.after(() => cleanup(dir));
|
|
git(['init', '-q'], dir);
|
|
git(['config', 'user.email', 'test@example.com'], dir);
|
|
git(['config', 'user.name', 'Test'], dir);
|
|
fs.writeFileSync(path.join(dir, 'a.txt'), 'first');
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'first commit'], dir);
|
|
// rename the default branch to "next" so it's a plain LOCAL branch, not
|
|
// a remote-tracking origin/next ref -- mirrors msd-test's sandbox shape.
|
|
git(['branch', '-M', 'next'], dir);
|
|
|
|
process.env.GITHUB_EVENT_NAME = 'push';
|
|
assert.strictEqual(resolveBaselineRef(dir), 'next');
|
|
});
|
|
|
|
test('nothing resolves at all (no env vars, not a git repo) -> returns ""', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-nongit-');
|
|
t.after(() => cleanup(dir));
|
|
assert.strictEqual(resolveBaselineRef(dir), '');
|
|
});
|
|
});
|
|
|
|
// ─── extractBaselineTree ─────────────────────────────────────────────────────
|
|
|
|
describe('extractBaselineTree', () => {
|
|
test('extracts package.json + package-lock.json at root from a real commit', (t) => {
|
|
const pkgContent = JSON.stringify({ name: 'root-fixture' });
|
|
const lockContent = JSON.stringify({ lockfileVersion: 3, name: 'root-fixture' });
|
|
const { dir, commitSha } = makeCommittedFixtureRepo(t, { pkgContent, lockContent });
|
|
|
|
const extracted = extractBaselineTree(commitSha, dir);
|
|
assert.notStrictEqual(extracted, null);
|
|
t.after(() => cleanup(extracted));
|
|
|
|
assert.strictEqual(fs.readFileSync(path.join(extracted, 'package.json'), 'utf-8'), pkgContent);
|
|
assert.strictEqual(fs.readFileSync(path.join(extracted, 'package-lock.json'), 'utf-8'), lockContent);
|
|
});
|
|
|
|
test('extracts package.json + package-lock.json from a subdir', (t) => {
|
|
const pkgContent = JSON.stringify({ name: 'sdk-fixture' });
|
|
const lockContent = JSON.stringify({ lockfileVersion: 3, name: 'sdk-fixture' });
|
|
const { dir, commitSha } = makeCommittedFixtureRepo(t, { subdir: 'sdk', pkgContent, lockContent });
|
|
|
|
const extracted = extractBaselineTree(commitSha, dir, 'sdk');
|
|
assert.notStrictEqual(extracted, null);
|
|
t.after(() => cleanup(extracted));
|
|
|
|
assert.strictEqual(fs.readFileSync(path.join(extracted, 'package.json'), 'utf-8'), pkgContent);
|
|
assert.strictEqual(fs.readFileSync(path.join(extracted, 'package-lock.json'), 'utf-8'), lockContent);
|
|
});
|
|
|
|
test('a ref that does not exist -> null', (t) => {
|
|
const { dir } = makeCommittedFixtureRepo(t);
|
|
const bogusSha = 'f'.repeat(40);
|
|
assert.strictEqual(extractBaselineTree(bogusSha, dir), null);
|
|
});
|
|
|
|
test('ref exists but package-lock.json was never committed at that ref -> null', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-nolock-');
|
|
t.after(() => cleanup(dir));
|
|
git(['init', '-q'], dir);
|
|
git(['config', 'user.email', 'test@example.com'], dir);
|
|
git(['config', 'user.name', 'Test'], dir);
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"nolock"}');
|
|
git(['add', '-A'], dir);
|
|
git(['commit', '-q', '-m', 'no lockfile'], dir);
|
|
const sha = git(['rev-parse', 'HEAD'], dir);
|
|
|
|
assert.strictEqual(extractBaselineTree(sha, dir), null);
|
|
});
|
|
});
|
|
|
|
// ─── runPackageLockAudit (filesystem-only skip conditions, no registry) ────
|
|
|
|
describe('runPackageLockAudit', () => {
|
|
test('missing package.json -> null', () => {
|
|
const dir = createTempDir('msd-audit-baseline-empty-');
|
|
try {
|
|
assert.strictEqual(runPackageLockAudit(dir), null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('package.json present but no package-lock.json -> null', () => {
|
|
const dir = createTempDir('msd-audit-baseline-nolock2-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"nolock2"}');
|
|
assert.strictEqual(runPackageLockAudit(dir), null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── runInstalledTreeAudit (filesystem-only skip conditions, no registry) ──
|
|
|
|
describe('runInstalledTreeAudit', () => {
|
|
test('missing package.json -> null', () => {
|
|
const dir = createTempDir('msd-audit-installed-empty-');
|
|
try {
|
|
assert.strictEqual(runInstalledTreeAudit(dir), null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
|
|
test('package.json present but no node_modules -> null', () => {
|
|
const dir = createTempDir('msd-audit-installed-nomodules-');
|
|
try {
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"nomodules"}');
|
|
assert.strictEqual(runInstalledTreeAudit(dir), null);
|
|
} finally {
|
|
cleanup(dir);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── isTimeoutKill ───────────────────────────────────────────────────────────
|
|
|
|
describe('isTimeoutKill', () => {
|
|
test('killed: true -> true', () => {
|
|
assert.strictEqual(isTimeoutKill({ killed: true }), true);
|
|
});
|
|
|
|
test('signal set (e.g. SIGTERM) -> true', () => {
|
|
assert.strictEqual(isTimeoutKill({ signal: 'SIGTERM' }), true);
|
|
});
|
|
|
|
test('both killed and signal set -> true', () => {
|
|
assert.strictEqual(isTimeoutKill({ killed: true, signal: 'SIGTERM' }), true);
|
|
});
|
|
|
|
test('neither killed nor signal set (normal non-zero exit) -> false', () => {
|
|
assert.strictEqual(isTimeoutKill({ killed: false, signal: null, status: 1, stdout: '{}' }), false);
|
|
});
|
|
|
|
test('killed: false explicitly -> false', () => {
|
|
assert.strictEqual(isTimeoutKill({ killed: false }), false);
|
|
});
|
|
|
|
test('null/undefined error -> false, does not throw', () => {
|
|
assert.strictEqual(isTimeoutKill(null), false);
|
|
assert.strictEqual(isTimeoutKill(undefined), false);
|
|
});
|
|
|
|
test('plain object with no killed/signal keys at all -> false', () => {
|
|
assert.strictEqual(isTimeoutKill({}), false);
|
|
});
|
|
});
|
|
|
|
// ─── buildTimeoutKillError ───────────────────────────────────────────────────
|
|
|
|
describe('buildTimeoutKillError', () => {
|
|
test('default attempts (1) uses singular ms-based phrasing, not "N attempts"', () => {
|
|
const err = buildTimeoutKillError('/some/dir', { stderr: 'some stderr text' });
|
|
assert.match(err.message, /npm audit timed out after \d+ms/);
|
|
assert.doesNotMatch(err.message, /attempts/);
|
|
assert.match(err.message, /some stderr text/);
|
|
});
|
|
|
|
test('attempts > 1 uses plural "N attempts" phrasing with backoff mention', () => {
|
|
const err = buildTimeoutKillError('/some/dir', { stderr: '' }, 3);
|
|
assert.match(err.message, /npm audit timed out after 3 attempts/);
|
|
assert.match(err.message, /exponential backoff/);
|
|
});
|
|
|
|
test('no error object at all still produces a message, no crash', () => {
|
|
const err = buildTimeoutKillError('/some/dir', undefined);
|
|
assert.match(err.message, /npm audit timed out after \d+ms/);
|
|
assert.match(err.message, /no stderr was captured/);
|
|
});
|
|
});
|
|
|
|
// ─── runPackageLockAudit -- timeout-kill classification (#4250) ─────────────
|
|
|
|
describe('runPackageLockAudit — timeout-kill retry classification (#4250, #4260)', () => {
|
|
function makeFixtureDir(t) {
|
|
const dir = createTempDir('msd-audit-baseline-timeout-');
|
|
t.after(() => cleanup(dir));
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"fixture"}');
|
|
fs.writeFileSync(path.join(dir, 'package-lock.json'), '{"lockfileVersion":3}');
|
|
return dir;
|
|
}
|
|
|
|
function makeKilledError() {
|
|
return Object.assign(new Error('command timed out'), {
|
|
killed: true,
|
|
signal: 'SIGTERM',
|
|
stdout: '{"auditReportVersion":2,"vulnerabi', // deliberately truncated, non-empty
|
|
stderr: 'npm http fetch GET 200 https://registry.npmjs.org/-/npm/v1/security/advisories/bulk (attempt 1) 178234ms',
|
|
});
|
|
}
|
|
|
|
test('a timeout-killed execFileSync call on every attempt throws a clear timeout error after exhausting retries, not a JSON parse error', (t) => {
|
|
const dir = makeFixtureDir(t);
|
|
const execFileSyncImpl = () => { throw makeKilledError(); };
|
|
const sleepImpl = () => {};
|
|
|
|
assert.throws(
|
|
() => runPackageLockAudit(dir, { execFileSyncImpl, sleepImpl }),
|
|
(err) => {
|
|
assert.match(err.message, /npm audit timed out after \d+ attempts/);
|
|
assert.match(err.message, /status\.npmjs\.org/);
|
|
assert.doesNotMatch(err.message, /Unexpected end of JSON input/);
|
|
assert.match(err.message, /Captured stderr before the last kill/);
|
|
assert.match(err.message, /npm http fetch GET/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test('a timeout-killed call with no captured stderr still produces a clear message (no crash on missing stderr)', (t) => {
|
|
const dir = makeFixtureDir(t);
|
|
const killedError = Object.assign(new Error('command timed out'), {
|
|
killed: true,
|
|
signal: 'SIGTERM',
|
|
// no stdout, no stderr at all
|
|
});
|
|
const execFileSyncImpl = () => { throw killedError; };
|
|
const sleepImpl = () => {};
|
|
|
|
assert.throws(
|
|
() => runPackageLockAudit(dir, { execFileSyncImpl, sleepImpl }),
|
|
(err) => {
|
|
assert.match(err.message, /npm audit timed out after \d+ attempts/);
|
|
assert.match(err.message, /no stderr was captured/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test('retry recovers: timeouts on the first attempts followed by a successful final attempt succeeds', (t) => {
|
|
const dir = makeFixtureDir(t);
|
|
const completeJson = JSON.stringify({ metadata: { vulnerabilities: { high: 0 } }, vulnerabilities: {} });
|
|
let calls = 0;
|
|
const execFileSyncImpl = () => {
|
|
calls += 1;
|
|
if (calls < 3) throw makeKilledError();
|
|
return completeJson;
|
|
};
|
|
const sleepImpl = () => {};
|
|
|
|
const result = runPackageLockAudit(dir, { execFileSyncImpl, sleepImpl });
|
|
assert.deepStrictEqual(result.metadata.vulnerabilities, { high: 0 });
|
|
assert.strictEqual(calls, 3);
|
|
});
|
|
|
|
test('a normal non-zero exit with complete stdout JSON still recovers correctly (no regression)', (t) => {
|
|
const dir = makeFixtureDir(t);
|
|
const completeJson = JSON.stringify({ metadata: { vulnerabilities: { high: 1 } }, vulnerabilities: { foo: {} } });
|
|
const nonZeroExitError = Object.assign(new Error('npm audit found vulnerabilities'), {
|
|
status: 1,
|
|
stdout: completeJson,
|
|
});
|
|
const execFileSyncImpl = () => { throw nonZeroExitError; };
|
|
|
|
const result = runPackageLockAudit(dir, { execFileSyncImpl });
|
|
assert.deepStrictEqual(result.metadata.vulnerabilities, { high: 1 });
|
|
});
|
|
|
|
test('a real successful call (no throw) still works via the injected impl', (t) => {
|
|
const dir = makeFixtureDir(t);
|
|
const completeJson = JSON.stringify({ metadata: { vulnerabilities: {} }, vulnerabilities: {} });
|
|
const execFileSyncImpl = () => completeJson;
|
|
|
|
const result = runPackageLockAudit(dir, { execFileSyncImpl });
|
|
assert.deepStrictEqual(result.metadata.vulnerabilities, {});
|
|
});
|
|
});
|
|
|
|
// ─── runNpmAuditWithRetry — backoff timing (#4260) ──────────────────────────
|
|
|
|
describe('runNpmAuditWithRetry — backoff timing (#4260)', () => {
|
|
test('a timeout-then-recover attempt sequence sleeps once with the base backoff value', (t) => {
|
|
const dir = createTempDir('msd-audit-baseline-backoff-');
|
|
t.after(() => cleanup(dir));
|
|
const completeJson = JSON.stringify({ metadata: { vulnerabilities: {} }, vulnerabilities: {} });
|
|
let calls = 0;
|
|
const execFileSyncImpl = () => {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
throw Object.assign(new Error('command timed out'), { killed: true, signal: 'SIGTERM' });
|
|
}
|
|
return completeJson;
|
|
};
|
|
const sleepCalls = [];
|
|
const sleepImpl = (ms) => sleepCalls.push(ms);
|
|
|
|
const parsed = runNpmAuditWithRetry(dir, ['audit', '--json'], { execFileSyncImpl, sleepImpl });
|
|
|
|
assert.deepStrictEqual(parsed.metadata.vulnerabilities, {});
|
|
assert.deepStrictEqual(sleepCalls, [AUDIT_BACKOFF_BASE_MS * 1]);
|
|
});
|
|
});
|