import { chmod, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { fragmentInventoryEvidence } from "../../fixtures/artifactEvidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { inventoryArtifact } from "../../fixtures/artifactInventory.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js"; import { artifactComparisonResultSchema, compareArtifacts, } from "../../../src/domain/artifactComparison.js"; import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js"; import { createEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { jsonValueSchema } from "../../../src/domain/jsonValue.js"; const PROVIDER = { id: "rea-artifact-graph", name: "REA artifact graph", version: "1", } as const; const observe = async (path: string) => { const inventory = await inventoryArtifact(path); return createEvidence( { path, sha256: inventory.manifest.root_sha256, format: inventory.manifest.root_format, }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse(inventory), confidence: "observed", authority: "shipped-artifact", }, ); }; const inspectPages = ( pages: readonly Evidence[], substepsCompleted = pages.length, ) => { const first = pages[0]; if (first === undefined) throw new Error("Missing inspection page"); const inventory = artifactInventoryResultSchema.parse( first.normalized_result, ); return createEvidence( { path: "artifact-inspection-example", sha256: inventory.manifest.root_sha256, format: inventory.manifest.root_format, }, PROVIDER, { operation: "inspect_artifact", parameters: {}, result: jsonValueSchema.parse({ inspection_id: `ai_${"f".repeat(64)}`, subject: { manifest_id: inventory.manifest.manifest_id, root_artifact_id: inventory.manifest.root_artifact_id, root_sha256: inventory.manifest.root_sha256, root_format: inventory.manifest.root_format, }, substeps: pages.map((evidence, index) => ({ substep_id: `ais_${(index + 1).toString(16).padStart(64, "0")}`, operation: "inventory_artifact", status: "completed", evidence_id: evidence.evidence_id, evidence, })), observations: [], hypotheses: [], contradictions: [], unexplored_branches: [], next_probes: [], coverage: { status: "complete-within-substeps", substeps_completed: substepsCompleted, observations_retained: 0, observations_omitted: 0, hypotheses_retained: 0, hypotheses_omitted: 0, unexplored_branches_retained: 0, unexplored_branches_omitted: 0, next_probes_retained: 0, next_probes_omitted: 0, }, evidence_links: pages.map(({ evidence_id }) => evidence_id), limitations: [], }), confidence: "observed", authority: "shipped-artifact", }, ); }; const changedArtifactComparison = async () => { const parent = await createTestTempDirectory("rea-artifact-compare-"); const leftPath = join(parent, "left.app"); const rightPath = join(parent, "right.app"); await Promise.all([mkdir(leftPath), mkdir(rightPath)]); await Promise.all([ writeFile(join(leftPath, "main.js"), "old();"), writeFile(join(leftPath, "same.txt"), "same"), writeFile(join(rightPath, "main.js"), "newer();"), writeFile(join(rightPath, "same.txt"), "same"), writeFile(join(rightPath, "added.txt"), "added"), ]); const left = await observe(leftPath); const right = await observe(rightPath); return { left, right, result: compareArtifacts(left, right), }; }; describe("leaf artifact comparison", () => { it("rejects inventory Evidence without required producer coverage", async () => { const root = await createTestTempDirectory("rea-artifact-plist-compare-"); const leftPath = join(root, "left.plist"); const xml = 'labelhello'; await writeFile(leftPath, xml); const left = await observe(leftPath); const parsed = artifactInventoryResultSchema.parse(left.normalized_result); const { coverage: _coverage, ...withoutCoverage } = parsed; const malformed = createEvidence( { path: left.subject!.local_path, sha256: left.subject!.digest.sha256, format: left.subject!.format, }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse(withoutCoverage), confidence: "observed", authority: "shipped-artifact", }, ); expect(() => parseArtifactInventoryEvidence(malformed)).toThrow(); }); it("keeps content and executable changes visible for complete plist roots", async () => { const root = await createTestTempDirectory("rea-artifact-plist-deltas-"); const leftPath = join(root, "left.plist"); const rightPath = join(root, "right.plist"); const plist = (label: string) => `label${label}`; await Promise.all([ writeFile(leftPath, plist("hello"), { mode: 0o644 }), writeFile(rightPath, plist("world"), { mode: 0o644 }), ]); const [left, changed] = await Promise.all([ observe(leftPath), observe(rightPath), ]); expect(compareArtifacts(left, changed)).toMatchObject({ status: "changed", summary: { changed: 1, unknown: 0 }, changes: [ expect.objectContaining({ logical_path: ".", classification: "changed", dimensions: expect.arrayContaining(["content"]), }), ], }); if (process.platform !== "win32") { await writeFile(rightPath, plist("hello"), { mode: 0o644 }); await chmod(rightPath, 0o755); const executable = await observe(rightPath); expect(compareArtifacts(left, executable)).toMatchObject({ status: "changed", summary: { changed: 1, unknown: 0 }, changes: [ expect.objectContaining({ logical_path: ".", classification: "changed", dimensions: expect.arrayContaining(["executable"]), }), ], }); } }); }); describe("artifact inspection inventory substeps", () => { it("assembles every paged inventory substep and retains its outer Evidence once", async () => { const root = await createTestTempDirectory("rea-inspect-pages-"); const path = join(root, "fixture.app"); await mkdir(path); await Promise.all([ writeFile(join(path, "first.js"), "first();"), writeFile(join(path, "second.js"), "second();"), ]); const source = await observe(path); const pages = fragmentInventoryEvidence(source, 2); const inspection = inspectPages(pages); const parsed = parseArtifactInventoryEvidence(inspection); expect(parsed.evidence).toEqual([inspection]); expect(parsed.inventory.complete).toBe(true); expect(parsed.inventory.nodes).toHaveLength( artifactInventoryResultSchema.parse(source.normalized_result).nodes .length, ); }); it("rejects inspection substeps with different inventory manifests", async () => { const root = await createTestTempDirectory("rea-inspect-mixed-pages-"); const firstPath = join(root, "first.plist"); const secondPath = join(root, "second.plist"); await Promise.all([ writeFile( firstPath, 'a1', ), writeFile( secondPath, 'b2', ), ]); const inspection = inspectPages([ await observe(firstPath), await observe(secondPath), ]); expect(() => parseArtifactInventoryEvidence(inspection)).toThrow( "Artifact inventory pages do not share one manifest", ); }); it("rejects inspection coverage counts that disagree with retained substeps", async () => { const root = await createTestTempDirectory("rea-inspect-count-mismatch-"); const path = join(root, "fixture.app"); await mkdir(path); await Promise.all([ writeFile(join(path, "first.js"), "first();"), writeFile(join(path, "second.js"), "second();"), ]); const pages = fragmentInventoryEvidence(await observe(path), 2); const inspection = inspectPages(pages, 1); expect(() => parseArtifactInventoryEvidence(inspection)).toThrow( "Artifact inspection substep count does not match its coverage declaration", ); }); it("authenticates later inventory substeps instead of silently ignoring them", async () => { const root = await createTestTempDirectory("rea-inspect-invalid-page-"); const path = join(root, "fixture.app"); await mkdir(path); await writeFile(join(path, "main.js"), "main();"); const source = await observe(path); const [first, later] = fragmentInventoryEvidence(source, 2); if (first === undefined || later === undefined) throw new Error("Missing inventory page"); const graph = artifactInventoryResultSchema.parse(later.normalized_result); const tamperedLater: Evidence = { ...later, normalized_result: jsonValueSchema.parse({ ...graph, limitations: ["Changed after this Evidence identifier was computed."], }), }; const inspection = inspectPages([first, tamperedLater]); expect(() => parseArtifactInventoryEvidence(inspection)).toThrow( "Evidence semantic identifier does not match its record", ); }); }); describe("artifact inventory coverage", () => { it("uses current unknown coverage without deriving it from limitation prose", async () => { const root = await createTestTempDirectory("rea-artifact-coverage-"); const path = join(root, "complete.plist"); await writeFile( path, '', ); const complete = await observe(path); const graph = artifactInventoryResultSchema.parse( complete.normalized_result, ); const unknownCoverage = createEvidence( { path: complete.subject!.local_path, sha256: complete.subject!.digest.sha256, format: complete.subject!.format, }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse({ ...graph, coverage: { scope: "root", status: "unknown" }, limitations: ["No extra child entries were observed."], }), confidence: "derived", authority: "shipped-artifact", }, ); expect( parseArtifactInventoryEvidence(unknownCoverage).inventory.complete, ).toBe(true); expect(compareArtifacts(unknownCoverage, unknownCoverage)).toMatchObject({ status: "unknown", summary: { unchanged: 0, unknown: 1 }, }); }); it("rejects inventory fragments that disagree on producer coverage", async () => { const parent = await createTestTempDirectory("rea-artifact-page-coverage-"); const path = join(parent, "fixture.app"); await mkdir(path); await writeFile(join(path, "main.js"), "main();"); const source = await observe(path); const fragments = fragmentInventoryEvidence(source, 2); const firstFragment = fragments[0]; const fragment = fragments[1]; if (firstFragment === undefined || fragment === undefined) throw new Error("Missing test fragment"); const fragmentGraph = artifactInventoryResultSchema.parse( fragment.normalized_result, ); const mixed = createEvidence( { path: fragment.subject!.local_path, sha256: fragment.subject!.digest.sha256, format: fragment.subject!.format, }, PROVIDER, { operation: "inventory_artifact", parameters: fragment.parameters, result: jsonValueSchema.parse({ ...fragmentGraph, coverage: { ...fragmentGraph.coverage, status: "unknown" }, }), confidence: "derived", authority: "shipped-artifact", }, ); expect(() => parseArtifactInventoryEvidence([firstFragment, mixed]), ).toThrow("Artifact inventory pages do not share one coverage declaration"); }); }); describe("artifact comparison", () => { it("keeps the canonical public example valid and fully comparable", () => { const left = parseArtifactInventoryEvidence( ARTIFACT_COMPARISON_EXAMPLE.left, ); const right = parseArtifactInventoryEvidence( ARTIFACT_COMPARISON_EXAMPLE.right, ); expect(left.inventory).toMatchObject({ complete: true, coverage: { scope: "root", status: "complete" }, }); expect(right.inventory).toMatchObject({ complete: true, coverage: { scope: "root", status: "complete" }, }); expect( artifactComparisonResultSchema.parse( compareArtifacts( ARTIFACT_COMPARISON_EXAMPLE.left, ARTIFACT_COMPARISON_EXAMPLE.right, ), ), ).toMatchObject({ status: "changed", summary: { changed: 1, unknown: 0 }, changes: [ expect.objectContaining({ logical_path: ".", classification: "changed", dimensions: expect.arrayContaining(["content"]), }), ], }); }); it("classifies deterministic path changes and cites both inventories", async () => { const firstObservation = await changedArtifactComparison(); const secondObservation = await changedArtifactComparison(); const first = firstObservation.result; const second = secondObservation.result; expect(first).toEqual(second); expect(artifactComparisonResultSchema.parse(first)).toMatchObject({ status: "changed", summary: { added: 1, changed: 2, unknown: 0 }, changes: expect.arrayContaining([ expect.objectContaining({ logical_path: "added.txt" }), ]), }); expect(first.changes[0]?.evidence_links).toEqual([ firstObservation.left.evidence_id, firstObservation.right.evidence_id, ]); expect(first.changes).toEqual( expect.arrayContaining([ expect.objectContaining({ logical_path: "added.txt", classification: "added", }), expect.objectContaining({ logical_path: "main.js", classification: "changed", dimensions: expect.arrayContaining(["content", "size"]), }), ]), ); }); it("reports incomplete inventory as truncated, never unchanged", async () => { const root = await createTestTempDirectory("rea-artifact-truncated-"); await writeFile(join(root, "one.txt"), "one"); const complete = await observe(root); const completeInventory = artifactInventoryResultSchema.parse( complete.normalized_result, ); const incomplete = createEvidence( { path: root, sha256: complete.subject?.digest.sha256 ?? "0".repeat(64), format: "directory", }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse({ ...completeInventory, nodes: [], }), confidence: "observed", authority: "shipped-artifact", limitations: ["Inventory node observations are unavailable."], }, ); const comparison = compareArtifacts(incomplete, incomplete); expect(comparison).toMatchObject({ status: "truncated", summary: { unchanged: 0, unknown: 2 }, }); expect(comparison.limitations).toContain( "Left artifact inventory is incomplete.", ); }); }); describe("artifact comparison completeness", () => { it("does not infer child changes from root-only coverage", async () => { const root = await createTestTempDirectory("rea-artifact-root-coverage-"); const leftPath = join(root, "left.app"); const rightPath = join(root, "right.app"); await Promise.all([mkdir(leftPath), mkdir(rightPath)]); await Promise.all([ writeFile(join(leftPath, "left-only.txt"), "left"), writeFile(join(rightPath, "right-only.txt"), "right"), ]); const [left, right] = await Promise.all([ observe(leftPath), observe(rightPath), ]); const rootOnly = (evidence: Awaited>) => { const inventory = artifactInventoryResultSchema.parse( evidence.normalized_result, ); return createEvidence( { path: evidence.subject?.local_path ?? "", sha256: inventory.manifest.root_sha256, format: inventory.manifest.root_format, }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse({ ...inventory, coverage: { scope: "root", status: "complete" }, }), confidence: "derived", authority: "shipped-artifact", }, ); }; const compared = compareArtifacts(rootOnly(left), rootOnly(right)); expect(compared.status).toBe("unknown"); expect(compared.summary).toMatchObject({ added: 0, removed: 0 }); expect( compared.changes .filter(({ logical_path }) => logical_path !== ".") .map(({ logical_path, classification }) => ({ logical_path, classification, })), ).toEqual([ { logical_path: "left-only.txt", classification: "unknown" }, { logical_path: "right-only.txt", classification: "unknown" }, ]); expect(compared.limitations).toContain( "Container entry coverage is unavailable for child paths.", ); }); it("compares every graph member for inventories larger than 500 entries", async () => { const root = await createTestTempDirectory("rea-artifact-pages-"); const entryCount = 501; await Promise.all( Array.from({ length: entryCount }, async (_, index) => writeFile( join(root, `file-${String(index).padStart(3, "0")}.txt`), String(index), ), ), ); const complete = await observe(root); const graph = artifactInventoryResultSchema.parse( complete.normalized_result, ); // The real risk above 500 entries is silent truncation, so assert the // manifest counts reconcile with the arrays actually returned and that // every written file is present. Deriving from `entryCount` keeps this // valid at any size instead of freezing a page boundary. expect(graph.manifest.node_count).toBe(graph.nodes.length); expect(graph.manifest.occurrence_count).toBe(graph.occurrences.length); expect(graph.manifest.edge_count).toBe(graph.edges.length); const written = new Set( Array.from( { length: entryCount }, (_, index) => `file-${String(index).padStart(3, "0")}.txt`, ), ); // Occurrences carry the inventory paths, so completeness is checked there. const paths = new Set( graph.occurrences.map((occurrence) => occurrence.logical_path), ); expect([...written].filter((path) => !paths.has(path))).toEqual([]); expect( graph.occurrences.filter( (occurrence) => occurrence.entry_kind === "file", ), ).toHaveLength(entryCount); expect(compareArtifacts(complete, complete)).toMatchObject({ status: "unchanged", summary: { unchanged: 502, unknown: 0 }, changes: [], }); }, 15_000); }); describe("artifact source-map relationship coverage", () => { it("preserves unknown source-map relationships for duplicate ZIP paths", async () => { const root = await createTestTempDirectory("rea-duplicate-source-map-"); const path = join(root, "duplicate-source-map.zip"); const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("main.js", new TextReader("first();"), { level: 0 }); await writer.add("your.js", new TextReader("second();"), { level: 0 }); await writer.add("main.js.map", new TextReader("{}"), { level: 0 }); const bytes = Buffer.from(await writer.close()); for (const [signature, headerSize] of [ [Buffer.from([0x50, 0x4b, 0x03, 0x04]), 30], [Buffer.from([0x50, 0x4b, 0x01, 0x02]), 46], ] as const) { const first = bytes.indexOf(signature); const second = bytes.indexOf(signature, first + 4); if (first < 0 || second <= first) throw new Error("Expected both source ZIP member headers"); expect( bytes.subarray(second + headerSize, second + headerSize + 7).toString(), ).toBe("your.js"); bytes.write("main.js", second + headerSize); } await writeFile(path, bytes); const evidence = await observe(path); const parsed = parseArtifactInventoryEvidence(evidence); const { inventory } = parsed; const map = inventory.occurrences.find( ({ logical_path }) => logical_path === "main.js.map", ); if (map === undefined) throw new Error("Expected source-map occurrence"); const mapEdge = inventory.edges.find( ({ occurrence_id }) => occurrence_id === map.occurrence_id, ); expect(inventory.coverage.status).toBe("unknown"); expect(map.limitations).toContain( 'Source-map target is unknown because multiple occurrences share "main.js".', ); expect(mapEdge).toMatchObject({ relation: "contains" }); expect(mapEdge?.relation).not.toBe("maps-source"); expect(inventory.integrityContradictions).toEqual([]); expect( inventory.limitations.some((limitation) => limitation.toLowerCase().includes("integrity"), ), ).toBe(false); }); }); describe("duplicate archive occurrence ownership", () => { it("keeps directory ancestry unknown when ZIP directory paths repeat", async () => { const root = await createTestTempDirectory( "rea-artifact-duplicate-directory-", ); const path = join(root, "duplicate-directories.zip"); const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("x/", new TextReader("")); await writer.add("y/", new TextReader("")); await writer.add("x/file.txt", new TextReader("member")); const bytes = Buffer.from(await writer.close()); for (const [signature, headerSize] of [ [Buffer.from([0x50, 0x4b, 0x03, 0x04]), 30], [Buffer.from([0x50, 0x4b, 0x01, 0x02]), 46], ] as const) { const first = bytes.indexOf(signature); const second = bytes.indexOf(signature, first + 4); if (first < 0 || second <= first) throw new Error("Expected both ZIP directory headers"); expect( bytes.subarray(second + headerSize, second + headerSize + 2).toString(), ).toBe("y/"); bytes.write("x/", second + headerSize); } await writeFile(path, bytes); const evidence = await observe(path); const inventory = parseArtifactInventoryEvidence(evidence).inventory; const directory = inventory.occurrences.filter( ({ logical_path }) => logical_path === "x", ); const child = inventory.occurrences.find( ({ logical_path }) => logical_path === "x/file.txt", ); expect(directory).toHaveLength(2); expect(directory.every(({ artifact_id }) => artifact_id === null)).toBe( true, ); expect(child?.parent_occurrence_id).toBeNull(); expect(child?.limitations).toContain( 'The parent occurrence for "x/file.txt" is unknown because multiple observed directory or container occurrences share "x".', ); expect(inventory.coverage.status).toBe("unknown"); }); it("retains duplicate path occurrences across pages and leaves pairing unknown", async () => { const root = await createTestTempDirectory("rea-artifact-duplicate-pages-"); const archives = [join(root, "left.zip"), join(root, "right.zip")] as const; for (const [index, path] of archives.entries()) { const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("x.txt", new TextReader("first")); await writer.add( "y.txt", new TextReader(index === 0 ? "second-left" : "second-right"), ); await writer.add("neighbor.txt", new TextReader("neighbor")); const bytes = Buffer.from(await writer.close()); // Produce a duplicate path the same way as real malformed/ambiguous ZIP // inputs: retain each member record and rewrite only the equal-length name. for (const [signature, headerSize] of [ [Buffer.from([0x50, 0x4b, 0x03, 0x04]), 30], [Buffer.from([0x50, 0x4b, 0x01, 0x02]), 46], ] as const) { const first = bytes.indexOf(signature); const second = bytes.indexOf(signature, first + 4); if (first < 0 || second <= first) throw new Error("Expected both ZIP member headers"); expect( bytes .subarray(second + headerSize, second + headerSize + 5) .toString(), ).toBe("y.txt"); bytes.write("x.txt", second + headerSize); } await writeFile(path, bytes); } const [left, right] = await Promise.all([ observe(archives[0]), observe(archives[1]), ]); const parsed = parseArtifactInventoryEvidence(left); const repeated = parsed.inventory.occurrences.filter( ({ logical_path }) => logical_path === "x.txt", ); expect(repeated).toHaveLength(2); expect( new Set(repeated.map(({ occurrence_id }) => occurrence_id)).size, ).toBe(2); const leftPages = fragmentInventoryEvidence(left, 2); const parsedPages = parseArtifactInventoryEvidence(leftPages); expect( parsedPages.inventory.occurrences.filter( ({ logical_path }) => logical_path === "x.txt", ), ).toHaveLength(2); const comparison = compareArtifacts( leftPages, fragmentInventoryEvidence(right, 2), ); expect(comparison).toMatchObject({ status: "unknown", summary: { unknown: expect.any(Number) }, changes: expect.arrayContaining([ expect.objectContaining({ logical_path: "x.txt", classification: "unknown", dimensions: ["availability"], left_occurrence_id: null, right_occurrence_id: null, left_artifact_id: null, right_artifact_id: null, }), ]), }); expect(comparison.limitations).toContain( "Duplicate logical paths cannot be paired across inventories; comparisons for those paths are unknown.", ); // Occurrences on the unknown duplicate path are never counted unchanged. // The archives differ, so the root changes and only neighbor.txt remains. expect(comparison.summary).toMatchObject({ unchanged: 1, unknown: 1 }); expect(compareArtifacts(left, left).summary).toMatchObject({ unchanged: 2, unknown: 1, }); }); }); describe("artifact comparison fragment integrity", () => { it("assembles real inventory fragments and retains every citation", async () => { const parent = await createTestTempDirectory("rea-artifact-fragments-"); const leftPath = join(parent, "left.app"); const rightPath = join(parent, "right.app"); await Promise.all([mkdir(leftPath), mkdir(rightPath)]); // Cross the former 100-citation limit with distinct, real graph members. const fragmentCount = 101; await Promise.all( Array.from({ length: fragmentCount }, async (_, index) => { await Promise.all([ writeFile(join(leftPath, `file-${index}.js`), `left(${index});`), writeFile(join(rightPath, `file-${index}.js`), `right(${index});`), ]); }), ); const left = await observe(leftPath); const right = await observe(rightPath); const leftFragments = fragmentInventoryEvidence(left, fragmentCount); const rightFragments = fragmentInventoryEvidence(right, fragmentCount); const citations = [...leftFragments, ...rightFragments] .map(({ evidence_id }) => evidence_id) .sort(); expect(new Set(citations).size).toBe(fragmentCount * 2); const comparison = compareArtifacts(leftFragments, rightFragments); const complete = compareArtifacts(left, right); expect( comparison.changes .filter(({ logical_path }) => logical_path !== ".") .map(({ logical_path, classification }) => ({ logical_path, classification, })) .sort((left, right) => left.logical_path.localeCompare(right.logical_path), ), ).toEqual( Array.from({ length: fragmentCount }, (_, index) => ({ logical_path: `file-${index}.js`, classification: "changed", })).sort((left, right) => left.logical_path.localeCompare(right.logical_path), ), ); expect({ ...comparison, changes: comparison.changes.map((change) => ({ ...change, evidence_links: [...change.evidence_links].sort(), })), }).toEqual({ ...complete, changes: complete.changes.map((change) => ({ ...change, evidence_links: citations, })), }); const fragment = leftFragments[0]; if (fragment === undefined) throw new Error("Expected inventory fragments"); const conflictingScope = createEvidence( { path: leftPath, sha256: left.subject?.digest.sha256 ?? "", format: "directory", }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse({ ...artifactInventoryResultSchema.parse(fragment.normalized_result), coverage: { scope: "root", status: "complete" }, }), }, ); expect(() => compareArtifacts( [conflictingScope, ...leftFragments.slice(1)], rightFragments, ), ).toThrow(/coverage declaration/u); }, 15_000); it("rejects non-inventory and tampered Evidence", async () => { const root = await createTestTempDirectory("rea-artifact-invalid-"); const evidence = await observe(root); expect(() => compareArtifacts({ ...evidence, operation: "binary_overview" }, evidence), ).toThrow(/identifier/u); const wrongOperation = createEvidence(undefined, PROVIDER, { operation: "binary_overview", parameters: {}, result: evidence.normalized_result, }); expect(() => compareArtifacts(wrongOperation, evidence)).toThrow( /inspect_artifact/u, ); const mismatchedSubject = createEvidence( { path: root, sha256: "f".repeat(64), format: "directory" }, PROVIDER, { operation: "inventory_artifact", parameters: {}, result: evidence.normalized_result, }, ); expect(() => compareArtifacts(mismatchedSubject, evidence)).toThrow( /root digest/u, ); }); }); describe("ZIP non-entry coverage", () => { it("retains pre-directory APK block changes under current ZIP coverage", async () => { const root = await createTestTempDirectory("rea-apk-block-compare-"); const writer = new ZipWriter(new Uint8ArrayWriter()); await writer.add("classes.dex", new TextReader("unchanged entry")); const zip = Buffer.from(await writer.close()); const eocd = zip.length - 22; expect(zip.readUInt32LE(eocd)).toBe(0x06054b50); const directory = zip.readUInt32LE(eocd + 16); // APK Signing Block framing with one opaque padding pair, not a verified signature. // https://source.android.com/docs/security/features/apksigning/v2#apk-signing-block const block = Buffer.alloc(48); block.writeBigUInt64LE(40n, 0); block.writeBigUInt64LE(8n, 8); block.writeUInt32LE(0x42726577, 16); block.writeBigUInt64LE(40n, 24); block.write("APK Sig Block 42", 32, "ascii"); const paths = [join(root, "before.apk"), join(root, "after.apk")]; for (const [index, path] of paths.entries()) { block.writeUInt32LE(index, 20); const apk = Buffer.concat([ zip.subarray(0, directory), block, zip.subarray(directory), ]); apk.writeUInt32LE(directory + block.length, eocd + block.length + 16); await writeFile(path, apk); } const left = await observe(paths[0]!); const right = await observe(paths[1]!); const inventory = artifactInventoryResultSchema.parse( left.normalized_result, ); expect(inventory.limitations).toContain( "ZIP inventory lists central-directory entries only. Non-entry bytes, including leading bytes before the first entry such as a self-extractor stub, an APK Signing Block before the central directory or a ZIP comment after it, are not occurrences.", ); expect( inventory.occurrences.map(({ logical_path }) => logical_path).sort(), ).toEqual([".", "classes.dex"]); expect(compareArtifacts(left, right)).toMatchObject({ status: "changed", summary: { changed: 1, unchanged: 1, unknown: 0 }, changes: [ expect.objectContaining({ logical_path: ".", dimensions: ["content"] }), ], }); }); });