qwen-code/scripts/tests/audit-runtime-critical.test.js
Shaojin Wen 0f56e35c0a
ci: keep the critical-audit gate honest when npm cannot answer (#7743)
npm retired the `security/audits/quick` endpoint, which now 400s for this
package tree. `npm audit` exits 1 for that exactly as it does for a real
critical finding, so every PR in the repo went red on 2026-07-26 with
"Invalid package tree" — a failure no branch can fix.

The exit code alone cannot gate a merge: treating every non-zero as
vulnerable blocks the repo on an npm outage, and ignoring it retires the
gate. The payload separates them — a real audit always carries
metadata.vulnerabilities, a transport failure carries the request error.

Classify on that: a finding still fails, an unreachable endpoint warns and
passes, and anything unrecognised fails closed so a payload-shape change
gets a human rather than a silently disabled gate.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 06:59:13 +00:00

129 lines
4.9 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { execFileSync, spawnSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { classifyAuditOutput } from '../audit-runtime-critical.js';
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
describe('scripts/audit-runtime-critical.js', () => {
it('separates a real audit from a registry that never answered', () => {
// npm exits 1 for BOTH, so the exit code cannot gate a merge on its own.
// This payload is what the retiring `security/audits/quick` endpoint
// actually returned on 2026-07-26, when it 400'd for every PR in the repo.
const endpointFailure = JSON.stringify({
message:
'400 Bad Request - POST https://registry.npmjs.org/-/npm/v1/security/audits/quick - Bad Request',
method: 'POST',
uri: 'https://registry.npmjs.org/-/npm/v1/security/audits/quick',
statusCode: 400,
error: 'Bad Request',
});
expect(classifyAuditOutput(endpointFailure)).toBe('unreachable');
// A real audit always carries the counts, whether or not anything was found.
const clean = JSON.stringify({
vulnerabilities: {},
metadata: { vulnerabilities: { critical: 0, high: 0, total: 0 } },
});
expect(classifyAuditOutput(clean)).toBe('audited');
const vulnerable = JSON.stringify({
vulnerabilities: { evil: {} },
metadata: { vulnerabilities: { critical: 2, high: 0, total: 2 } },
});
expect(classifyAuditOutput(vulnerable)).toBe('audited');
});
it('fails closed on output it does not recognise', () => {
// If npm changes its payload shape, the gate must go red and get a human
// to look — never quietly report success and retire itself.
expect(classifyAuditOutput('')).toBe('unreadable');
expect(classifyAuditOutput('not json at all')).toBe('unreadable');
expect(classifyAuditOutput(JSON.stringify({ unexpected: true }))).toBe(
'unreadable',
);
// Shape-only: an object without counts and without an error is NOT an
// excuse to pass, even though it parses.
expect(classifyAuditOutput(JSON.stringify({ metadata: {} }))).toBe(
'unreadable',
);
});
it('still fails the build on a real critical vulnerability', () => {
// The whole risk of this change is shipping a gate that never fires again.
// Drive the real script end to end against a stubbed `npm` on PATH, so the
// exit code being asserted is the one CI would actually see.
const shimDir = mkdtempSync(join(tmpdir(), 'audit-shim-'));
const runWithNpmStub = (payload, npmExit) => {
writeFileSync(
join(shimDir, 'npm'),
`#!/usr/bin/env bash\ncat <<'JSON'\n${payload}\nJSON\nexit ${npmExit}\n`,
{ mode: 0o755 },
);
const result = spawnSync(
process.execPath,
[join(root, 'scripts', 'audit-runtime-critical.js')],
{
encoding: 'utf8',
env: { ...process.env, PATH: `${shimDir}:${process.env.PATH}` },
},
);
return result.status;
};
try {
const counts = (critical) =>
JSON.stringify({
vulnerabilities: {},
metadata: { vulnerabilities: { critical, high: 0, total: critical } },
});
// A finding still blocks the merge...
expect(runWithNpmStub(counts(2), 1)).toBe(1);
// ...a clean audit still passes...
expect(runWithNpmStub(counts(0), 0)).toBe(0);
// ...only an unreachable endpoint is waved through...
expect(
runWithNpmStub(
JSON.stringify({ statusCode: 400, error: 'Bad Request' }),
1,
),
).toBe(0);
// ...and unrecognised output fails closed even when npm exits 0, so a
// payload-shape change can never silently retire the gate.
expect(runWithNpmStub('totally not json', 1)).toBe(1);
expect(runWithNpmStub('totally not json', 0)).toBe(1);
} finally {
rmSync(shimDir, { recursive: true, force: true });
}
});
it('is wired into the audit script and stays runnable', () => {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
expect(pkg.scripts['audit:runtime:critical']).toBe(
'node scripts/audit-runtime-critical.js',
);
// Importing the module must not shell out to npm — the main guard is what
// keeps this test file from running a real audit on import.
const printed = execFileSync(
process.execPath,
[
'-e',
`import(${JSON.stringify(
join(root, 'scripts', 'audit-runtime-critical.js'),
)}).then((m) => console.log(typeof m.classifyAuditOutput))`,
],
{ encoding: 'utf8' },
);
expect(printed.trim()).toBe('function');
});
});