diff --git a/scripts/audit-reviewed-npm-graph.mts b/scripts/audit-reviewed-npm-graph.mts index 9f82826cfae..2df2f9578e9 100755 --- a/scripts/audit-reviewed-npm-graph.mts +++ b/scripts/audit-reviewed-npm-graph.mts @@ -233,13 +233,27 @@ function materializeLockedGraph( fs.copyFileSync(sourcePackage, path.join(destination, "package.json")); fs.copyFileSync(sourceLock, path.join(destination, "package-lock.json")); run("npm", ["ci", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund"], destination); - verifyInstalledNpmLock({ + verifyMaterializedLockedGraph({ destination, expectedLockSha256, label: graph.label }); + return destination; +} + +export function verifyMaterializedLockedGraph({ + destination, + expectedLockSha256, + label, +}: Readonly<{ + destination: string; + expectedLockSha256: string; + label: string; +}>): readonly string[] { + const lockfilePath = path.join(destination, "package-lock.json"); + return verifyInstalledNpmLock({ expectedLockSha256, installRoot: destination, - label: graph.label, - lockfilePath: path.join(destination, "package-lock.json"), + label, + lockfilePath, + omitDev: true, }); - return destination; } export function selectReviewedLockSha256( @@ -510,13 +524,7 @@ function main(): void { const reports = [ { label: SOURCE_GRAPH.label, - result: auditSourceGraph( - config, - tempRoot, - exceptionFile, - artifactDirectory, - npmVersion, - ), + result: auditSourceGraph(config, tempRoot, exceptionFile, artifactDirectory, npmVersion), }, { label: "reviewed archive graph", diff --git a/test/reviewed-npm-audit-workflow.test.ts b/test/reviewed-npm-audit-workflow.test.ts index ab8321ffe81..7dd961423f7 100644 --- a/test/reviewed-npm-audit-workflow.test.ts +++ b/test/reviewed-npm-audit-workflow.test.ts @@ -15,6 +15,7 @@ import { normalizeOpenClawSignatureAlias, parseAuditConfig, selectReviewedLockSha256, + verifyMaterializedLockedGraph, } from "../scripts/audit-reviewed-npm-graph.mts"; import { verifyInstalledNpmLock } from "../scripts/lib/reviewed-npm-archive.mts"; import type { AuditPolicyResult } from "../scripts/lib/reviewed-npm-audit.mts"; @@ -435,6 +436,38 @@ esac } }); + it("accepts an omitted development-only package in a locked production install (#8394)", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-locked-production-")); + const lockfilePath = path.join(root, "package-lock.json"); + const lockSource = `${JSON.stringify( + { + lockfileVersion: 3, + packages: { + "": { + devDependencies: { "@types/node": "25.5.2" }, + name: "locked-production-fixture", + version: "1.0.0", + }, + "node_modules/@types/node": { dev: true, version: "25.5.2" }, + }, + }, + null, + 2, + )}\n`; + try { + fs.writeFileSync(lockfilePath, lockSource); + expect( + verifyMaterializedLockedGraph({ + destination: root, + expectedLockSha256: createHash("sha256").update(lockSource).digest("hex"), + label: "locked production fixture", + }), + ).toEqual([]); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + it("rejects an unreviewed registry package before npm ci installs the root production graph (#8116)", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-source-graph-registry-")); const destination = path.join(root, "materialized");