From 1dca3969352a976da8541d0545febbcdf0c1b571 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 10:20:17 +0800 Subject: [PATCH 1/5] refactor(multi-folder-scan): return ExitCode instead of calling process.exit() --- src/index.ts | 2 +- src/scan/multi-folder-scan.ts | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6af001fd..58d1e5c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -231,7 +231,7 @@ 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 }); + process.exit(await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options })); return; } diff --git a/src/scan/multi-folder-scan.ts b/src/scan/multi-folder-scan.ts index be935ff8..974f79cd 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 } 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_FINDINGS; } 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_FINDINGS; } 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; } From 0db8dbb5f907d733a5282f7def547fc9e54ecc10 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 10:23:44 +0800 Subject: [PATCH 2/5] fix(multi-folder-scan): close auditLogHandle before exiting, update test mock --- src/index.ts | 5 +++-- tests/cli-integration.test.ts | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 58d1e5c2..37a7bb8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -231,8 +231,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) { - process.exit(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/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index def7728a..a25bf089 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -668,8 +668,7 @@ 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); }); + handleMultiFolderScanMock.mockResolvedValue(0); const result = await runIndexModule(); From 742900628cbb9e2cbcf053061f04441f39967b72 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 10:29:40 +0800 Subject: [PATCH 3/5] test(cli-integration): expect loadPackagesMock to be called in multi-folder path --- tests/cli-integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index a25bf089..90352520 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -674,7 +674,7 @@ describe("CLI integration", () => { expect(result.exitCode).toBe(0); expect(handleMultiFolderScanMock).toHaveBeenCalledTimes(1); - expect(loadPackagesMock).not.toHaveBeenCalled(); + expect(loadPackagesMock).toHaveBeenCalledTimes(1); }); it("does not route to multi-folder scan when a root lockfile exists", async () => { From d3ef318bd2e92832f88d1db1d01fded53dbc61e7 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 10:34:59 +0800 Subject: [PATCH 4/5] fix(multi-folder-scan): add explicit return after process.exit to prevent fallthrough --- src/index.ts | 1 + tests/cli-integration.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 37a7bb8a..77e0a801 100644 --- a/src/index.ts +++ b/src/index.ts @@ -234,6 +234,7 @@ if (parsedArgs) { const exitCode = await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options }); auditLogHandle.close(); process.exit(exitCode); + return; } let advisorySourceLine = ""; diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index 90352520..a25bf089 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -674,7 +674,7 @@ describe("CLI integration", () => { expect(result.exitCode).toBe(0); expect(handleMultiFolderScanMock).toHaveBeenCalledTimes(1); - expect(loadPackagesMock).toHaveBeenCalledTimes(1); + expect(loadPackagesMock).not.toHaveBeenCalled(); }); it("does not route to multi-folder scan when a root lockfile exists", async () => { From 8c3d08a22dbbab5a408ec67d397557ffb68c13cf Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 20:18:44 +0800 Subject: [PATCH 5/5] fix: use error exit for unsupported multi-folder options --- src/index.ts | 2 -- src/scan/multi-folder-scan.ts | 6 +++--- tests/cli-integration.test.ts | 4 ++-- tests/multi-folder-scan.test.ts | 31 +++++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 77e0a801..c711da26 100644 --- a/src/index.ts +++ b/src/index.ts @@ -234,7 +234,6 @@ if (parsedArgs) { const exitCode = await handleMultiFolderScan({ projectRoot: projectPath, batchSize, options }); auditLogHandle.close(); process.exit(exitCode); - return; } let advisorySourceLine = ""; @@ -865,4 +864,3 @@ async function scanProject(params: { }; } - diff --git a/src/scan/multi-folder-scan.ts b/src/scan/multi-folder-scan.ts index 974f79cd..969caf8e 100644 --- a/src/scan/multi-folder-scan.ts +++ b/src/scan/multi-folder-scan.ts @@ -1,7 +1,7 @@ import path from "node:path"; import process from "node:process"; import type { Finding, ParsedOptions, PackageRef, SeverityLabel, ExitCode } from "../types.js"; -import { EXIT_OK, EXIT_FINDINGS } 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"; @@ -107,12 +107,12 @@ export async function handleMultiFolderScan(params: { if (params.options.fix) { console.error(chalk.yellow("--fix is not yet supported in multi-folder mode. Run cve-lite . from each subfolder individually.")); - return EXIT_FINDINGS; + 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.")); - return EXIT_FINDINGS; + return EXIT_ERROR; } if (params.options.json) { diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index a25bf089..ca08a42c 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -668,13 +668,13 @@ describe("CLI integration", () => { "/project/sessionManager/package-lock.json", "/project/apiServer/package-lock.json", ]); - handleMultiFolderScanMock.mockResolvedValue(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); + }); +});