From c00739bb1f1ae3cb6f00464e8fd9f5b5f7dc6b6e Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Tue, 14 Jul 2026 19:23:00 +0800 Subject: [PATCH] perf(application): scan version artifacts in parallel --- src/application/CrossVersionInventory.ts | 60 ++++++++++--- tests/crossVersionInventory.test.ts | 103 +++++++++++++++++++++++ 2 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 tests/crossVersionInventory.test.ts diff --git a/src/application/CrossVersionInventory.ts b/src/application/CrossVersionInventory.ts index 49d0d34b7..ca1de2ca3 100644 --- a/src/application/CrossVersionInventory.ts +++ b/src/application/CrossVersionInventory.ts @@ -34,6 +34,43 @@ export interface InventoryEvidencePages { readonly right: readonly Evidence[]; } +/** Injectable artifact scan boundary used by paired version inventory. */ +export type VersionInventoryScanner = typeof scanAuthorizedArtifactInventory; + +/** Inputs shared by the two scans in one version comparison. */ +export interface VersionInventoryScanOptions { + readonly leftPath: string; + readonly rightPath: string; + readonly inputRoots: readonly string[]; + readonly limits: ArtifactLimits; + readonly signal?: AbortSignal; + readonly integrity: ArtifactIntegrityPolicy; +} + +/** Start both independent version scans while preserving left/right identity. */ +export const scanVersionInventories = async ( + options: VersionInventoryScanOptions, + scanner: VersionInventoryScanner = scanAuthorizedArtifactInventory, +): Promise => { + const [left, right] = await Promise.all([ + scanner( + options.leftPath, + options.inputRoots, + options.limits, + options.signal, + options.integrity, + ), + scanner( + options.rightPath, + options.inputRoots, + options.limits, + options.signal, + options.integrity, + ), + ]); + return { left, right }; +}; + /** Authorize and scan both inputs once under the same bounded traversal policy. */ export const scanVersions = async ( input: CrossVersionInvestigationInput, @@ -51,21 +88,16 @@ export const scanVersions = async ( enabled: integrityContinueEnabled, maxMismatches: input.max_integrity_mismatches, }; - const left = await scanAuthorizedArtifactInventory( - input.left_path, - inputRoots, - limits, - signal, - integrity, + return ok( + await scanVersionInventories({ + leftPath: input.left_path, + rightPath: input.right_path, + inputRoots, + limits, + ...(signal === undefined ? {} : { signal }), + integrity, + }), ); - const right = await scanAuthorizedArtifactInventory( - input.right_path, - inputRoots, - limits, - signal, - integrity, - ); - return ok({ left, right }); } catch (cause: unknown) { if (cause instanceof ArtifactReaderFailure) return err( diff --git a/tests/crossVersionInventory.test.ts b/tests/crossVersionInventory.test.ts new file mode 100644 index 000000000..6f34d675a --- /dev/null +++ b/tests/crossVersionInventory.test.ts @@ -0,0 +1,103 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { scanArtifactInventory } from "../src/application/ArtifactInventory.js"; +import { + scanVersionInventories, + type VersionInventoryScanner, +} from "../src/application/CrossVersionInventory.js"; + +const LIMITS = { + maxEntries: 10, + maxTotalBytes: 1_024, + maxEntryBytes: 512, + maxCompressionRatio: 10, + maxDepth: 8, + maxPathBytes: 256, +} as const; + +const INTEGRITY = { + mode: "fail", + approved: false, + enabled: false, + maxMismatches: 1, +} as const; + +const deferred = () => { + let resolvePromise: (value: Value) => void = () => undefined; + let rejectPromise: (reason: unknown) => void = () => undefined; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + return { promise, resolve: resolvePromise, reject: rejectPromise }; +}; + +describe("cross-version inventory", () => { + it("starts both scans together and preserves left/right result order", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-version-inventory-")); + try { + const artifact = join(root, "artifact.js"); + await writeFile(artifact, "export const value = 1;\n"); + const snapshot = await scanArtifactInventory(artifact, LIMITS); + const left = deferred(); + const right = deferred(); + const calls: string[] = []; + const controller = new AbortController(); + const scanner: VersionInventoryScanner = (...arguments_) => { + const [path, roots, limits, signal, integrity] = arguments_; + calls.push(path); + expect(roots).toEqual([root]); + expect(limits).toBe(LIMITS); + expect(signal).toBe(controller.signal); + expect(integrity).toBe(INTEGRITY); + return path === "left" ? left.promise : right.promise; + }; + + const result = scanVersionInventories( + { + leftPath: "left", + rightPath: "right", + inputRoots: [root], + limits: LIMITS, + signal: controller.signal, + integrity: INTEGRITY, + }, + scanner, + ); + + expect(calls).toEqual(["left", "right"]); + right.resolve(snapshot); + left.resolve(snapshot); + await expect(result).resolves.toEqual({ + left: snapshot, + right: snapshot, + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("rejects promptly when either parallel scan fails", async () => { + const left = deferred(); + const failure = new Error("right scan failed"); + const scanner: VersionInventoryScanner = (path) => + path === "left" ? left.promise : Promise.reject(failure); + + await expect( + scanVersionInventories( + { + leftPath: "left", + rightPath: "right", + inputRoots: ["/approved"], + limits: LIMITS, + integrity: INTEGRITY, + }, + scanner, + ), + ).rejects.toBe(failure); + }); +});