From 52bf709bcefb17ab07e1f9c47c5fadf5eca9bd4e Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Mon, 5 Oct 2026 06:42:56 -0700 Subject: [PATCH] fix(artifacts): exclude removed paths from unchanged counts (#569) --- src/domain/artifactComparison.ts | 3 +- .../artifactComparisonSummary.test.ts | 88 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/boundary/filesystem/artifactComparisonSummary.test.ts diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index 733465f47..6e2de7d40 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -403,7 +403,8 @@ const summarize = ( unchanged: covered ? Math.max( 0, - Math.min(left.occurrences.length, right.occurrences.length) - + left.occurrences.length - + counts.removed - counts.changed - counts.contradiction - counts.unknown, diff --git a/tests/boundary/filesystem/artifactComparisonSummary.test.ts b/tests/boundary/filesystem/artifactComparisonSummary.test.ts new file mode 100644 index 000000000..63266bc0b --- /dev/null +++ b/tests/boundary/filesystem/artifactComparisonSummary.test.ts @@ -0,0 +1,88 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js"; +import { compareArtifacts } from "../../../src/domain/artifactComparison.js"; +import { createEvidence } from "../../../src/domain/evidence.js"; +import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const observe = async (path: string) => { + const inventory = await inventoryArtifact(path); + return createEvidence( + { + path, + sha256: inventory.manifest.root_sha256, + format: inventory.manifest.root_format, + }, + { id: "rea-artifact-graph", name: "REA artifact graph", version: "1" }, + { + operation: "inventory_artifact", + parameters: {}, + result: jsonValueSchema.parse(inventory), + confidence: "observed", + authority: "shipped-artifact", + }, + ); +}; + +describe("artifact comparison summary", () => { + it.each([ + { removed: 1, added: 1 }, + { removed: 1, added: 2 }, + { removed: 2, added: 1 }, + { removed: 0, added: 1 }, + { removed: 1, added: 0 }, + { removed: 0, added: 0 }, + ])( + "counts unchanged paths with $removed removals and $added additions", + async ({ removed, added }) => { + const root = await createTestTempDirectory("rea-artifact-summary-"); + const leftPath = join(root, "left"); + const rightPath = join(root, "right"); + await Promise.all([mkdir(leftPath), mkdir(rightPath)]); + await Promise.all( + [leftPath, rightPath].flatMap((path) => + ["same.txt", "duplicate.txt"].map((name) => + writeFile(join(path, name), "unchanged content"), + ), + ), + ); + await Promise.all([ + ...Array.from({ length: removed }, (_, index) => + writeFile(join(leftPath, `removed-${index}.txt`), "old content"), + ), + ...Array.from({ length: added }, (_, index) => + writeFile(join(rightPath, `added-${index}.txt`), "new content"), + ), + ]); + const [left, right] = await Promise.all([ + observe(leftPath), + observe(rightPath), + ]); + const comparison = compareArtifacts(left, right); + const changed = removed + added > 0 ? 1 : 0; + + expect(comparison.summary).toEqual({ + unchanged: 3 - changed, + added, + removed, + changed, + unknown: 0, + contradiction: 0, + }); + expect(comparison.changes).toHaveLength(removed + added + changed); + expect(comparison.summary.unchanged + comparison.changes.length).toBe( + 3 + removed + added, + ); + expect( + comparison.changes.map(({ logical_path }) => logical_path), + ).not.toContain("same.txt"); + expect( + comparison.changes.map(({ logical_path }) => logical_path), + ).not.toContain("duplicate.txt"); + }, + ); +});