fix(artifacts): preserve integrity contradictions for one-sided members (#7076)

This commit is contained in:
Wenyu Chiou
2026-10-25 01:47:41 -04:00
committed by GitHub
parent 4973ce7202
commit fead4fdaa7
2 changed files with 60 additions and 15 deletions
+9 -5
View File
@@ -324,17 +324,20 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
evidence_links: [...input.evidenceLinks],
});
const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => {
const classifyPresence = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): ArtifactChange | null => {
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
if (input.leftOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
...base,
classification: covered ? "added" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
if (input.rightOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
...base,
classification: covered ? "removed" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
@@ -372,11 +375,12 @@ const classifyAvailability = (
};
const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => {
const presence = classifyPresence(input);
if (presence !== null) return presence;
const base = buildArtifactChangeBase(input);
// A declared/observed mismatch is known even without an opposite occurrence.
const integrity = classifyIntegrity(input, base);
if (integrity !== null) return integrity;
const presence = classifyPresence(input, base);
if (presence !== null) return presence;
const declaration = classifyPyInstallerDeclaration(input, base);
if (declaration.handled) return declaration.change;
const availability = classifyAvailability(input, base);
+51 -10
View File
@@ -21,6 +21,7 @@ import {
occurrenceIdForLocation,
} from "../../../src/domain/artifactIdentity.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js";
import {
ok as resultOk,
err as resultErr,
@@ -1258,6 +1259,9 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
const root = await createTestTempDirectory("rea-asar-attribution-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "keep.txt"), "unchanged sibling\n");
const absentArchive = join(root, "absent.asar");
await createPackageWithOptions(source, absentArchive, {});
const original = "console.log('ok');\n";
const changed = "console.log('no');\n";
await writeFile(join(source, "main.js"), original);
@@ -1280,7 +1284,12 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
await server.connect(st);
await client.connect(ct);
const observations = [];
for (const path of [originalArchive, staleArchive, currentArchive]) {
for (const path of [
originalArchive,
staleArchive,
currentArchive,
absentArchive,
]) {
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
@@ -1298,20 +1307,32 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(record).ok).toBe(true);
const [originalEvidence, stale, current] = observations;
const [originalEvidence, stale, current, absent] = observations;
if (
originalEvidence === undefined ||
stale === undefined ||
current === undefined
current === undefined ||
absent === undefined
)
throw new Error("Expected three artifact observations");
for (const [left, right, mismatches] of [
[stale, current, [stale.evidence_id]],
[current, stale, [stale.evidence_id]],
[stale, stale, [stale.evidence_id]],
[originalEvidence, current, []],
throw new Error("Expected four artifact observations");
for (const [left, right, mismatches, presence] of [
[stale, current, [stale.evidence_id], "changed"],
[current, stale, [stale.evidence_id], "changed"],
[stale, stale, [stale.evidence_id], "changed"],
[originalEvidence, current, [], "changed"],
[absent, stale, [stale.evidence_id], "added"],
[stale, absent, [stale.evidence_id], "removed"],
[absent, current, [], "added"],
[current, absent, [], "removed"],
] as const)
await assertIntegrityComparison(client, session, left, right, mismatches);
await assertIntegrityComparison(
client,
session,
left,
right,
mismatches,
presence,
);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
@@ -1323,6 +1344,7 @@ const assertIntegrityComparison = async (
left: ReturnType<typeof toolEvidence>,
right: ReturnType<typeof toolEvidence>,
mismatches: readonly string[],
presence: "added" | "removed" | "changed",
) => {
const compared = await client.callTool({
name: "compare_artifacts",
@@ -1332,12 +1354,31 @@ const assertIntegrityComparison = async (
const comparison = toolEvidence(compared.structuredContent);
expect(comparison.normalized_result).toMatchObject({
status: mismatches.length === 0 ? "changed" : "contradiction",
summary: { contradiction: mismatches.length === 0 ? 0 : 1 },
integrity_mismatch_evidence_ids: mismatches,
});
const unknown = session
.listUnknowns({ domain: "artifact-comparison" })
.find((item) => item.question.includes(comparison.evidence_id));
expect(unknown).toBeDefined();
const [leftMember, rightMember] = [left, right].map((record) =>
parseArtifactInventoryEvidence(record).inventory.occurrences.find(
({ logical_path }) => logical_path === "main.js",
),
);
expect(comparison.normalized_result).toMatchObject({
changes: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: mismatches.length > 0 ? "contradiction" : presence,
...(mismatches.length > 0 ? { dimensions: ["integrity"] } : {}),
left_occurrence_id: leftMember?.occurrence_id ?? null,
left_artifact_id: leftMember?.artifact_id ?? null,
right_occurrence_id: rightMember?.occurrence_id ?? null,
right_artifact_id: rightMember?.artifact_id ?? null,
}),
]),
});
if (mismatches.length > 0) {
expect(unknown?.contradicting_evidence_ids).toEqual(mismatches);
expect(unknown?.supporting_evidence_ids).toEqual(