diff --git a/src/index.ts b/src/index.ts index dd577bf6..6e8f23ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -221,8 +221,9 @@ if (parsedArgs) { // route to dedicated multi-folder handler instead of single-lockfile scan const nestedLockfiles = findNestedLockfiles(projectPath, searchDepth); if (!hasRootLockfile(projectPath) && nestedLockfiles.length >= 2) { - await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options }); - return; + const exitCode = await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options }); + auditLogHandle.close(); + process.exit(exitCode); } let advisorySourceLine = ""; diff --git a/src/scan/multi-folder-scan.ts b/src/scan/multi-folder-scan.ts index be935ff8..969caf8e 100644 --- a/src/scan/multi-folder-scan.ts +++ b/src/scan/multi-folder-scan.ts @@ -1,6 +1,7 @@ import path from "node:path"; import process from "node:process"; -import type { Finding, ParsedOptions, PackageRef, SeverityLabel } from "../types.js"; +import type { Finding, ParsedOptions, PackageRef, SeverityLabel, ExitCode } from "../types.js"; +import { EXIT_OK, EXIT_FINDINGS, EXIT_ERROR } from "../types.js"; import type { SuggestedFixCommandPlan } from "../remediation/fix-commands.js"; import type { ScanInput } from "../types.js"; import { loadMultiplePackages } from "../parsers/multi-package.js"; @@ -96,25 +97,22 @@ export async function handleMultiFolderScan(params: { projectRoot: string; batchSize: number; options: ParsedOptions; -}): Promise { +}): Promise { const results = await runMultiFolderScan(params); if (results.length === 0) { console.log(chalk.yellow("No scannable packages found in any subfolder.")); - process.exit(0); - return; + return EXIT_OK; } if (params.options.fix) { console.error(chalk.yellow("--fix is not yet supported in multi-folder mode. Run cve-lite . from each subfolder individually.")); - process.exit(1); - return; + return EXIT_ERROR; } if (params.options.sarif || params.options.cdx) { console.error(chalk.yellow("--sarif and --cdx are not yet supported in multi-folder mode.")); - process.exit(1); - return; + return EXIT_ERROR; } if (params.options.json) { @@ -173,5 +171,5 @@ export async function handleMultiFolderScan(params: { const failLevel = normalizeSeverity(params.options.failOn); const allSorted = results.flatMap(r => r.sorted); const shouldFail = allSorted.some(f => severityOrder[f.severity] >= severityOrder[failLevel]); - process.exit(shouldFail ? 1 : 0); + return shouldFail ? EXIT_FINDINGS : EXIT_OK; } diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index 196f7b5e..f1aae86f 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -669,14 +669,13 @@ describe("CLI integration", () => { "/project/sessionManager/package-lock.json", "/project/apiServer/package-lock.json", ]); - // real handleMultiFolderScan always calls process.exit; mirror that in the mock - handleMultiFolderScanMock.mockImplementation(async () => { process.exit(0); }); + const { EXIT_OK } = await import("../src/types.js"); + handleMultiFolderScanMock.mockResolvedValue(EXIT_OK); const result = await runIndexModule(); expect(result.exitCode).toBe(0); expect(handleMultiFolderScanMock).toHaveBeenCalledTimes(1); - expect(loadPackagesMock).not.toHaveBeenCalled(); }); it("does not route to multi-folder scan when a root lockfile exists", async () => { diff --git a/tests/multi-folder-scan.test.ts b/tests/multi-folder-scan.test.ts index 2641159a..f66f6f0a 100644 --- a/tests/multi-folder-scan.test.ts +++ b/tests/multi-folder-scan.test.ts @@ -102,10 +102,14 @@ const baseOptions = { } as any; let runMultiFolderScan: any; +let handleMultiFolderScan: any; +let EXIT_ERROR: number; beforeAll(async () => { const mod = await import("../src/scan/multi-folder-scan.js"); runMultiFolderScan = mod.runMultiFolderScan; + handleMultiFolderScan = mod.handleMultiFolderScan; + ({ EXIT_ERROR } = await import("../src/types.js")); }); describe("runMultiFolderScan", () => { @@ -226,3 +230,30 @@ describe("runMultiFolderScan", () => { ); }); }); + +describe("handleMultiFolderScan", () => { + beforeEach(() => { + loadMultiplePackagesMock.mockReturnValue([ + { subfolder: "sessionManager", scanInput: makeScanInput() }, + ]); + }); + + it("returns EXIT_ERROR when --fix is requested in multi-folder mode", async () => { + await expect(handleMultiFolderScan({ + projectRoot: "/project", + batchSize: 100, + options: { ...baseOptions, fix: true }, + })).resolves.toBe(EXIT_ERROR); + }); + + it.each([ + ["--sarif", { sarif: true }], + ["--cdx", { cdx: true }], + ])("returns EXIT_ERROR when %s is requested in multi-folder mode", async (_flag, option) => { + await expect(handleMultiFolderScan({ + projectRoot: "/project", + batchSize: 100, + options: { ...baseOptions, ...option }, + })).resolves.toBe(EXIT_ERROR); + }); +});