fix(cli): recognize integrity failures with partial observations (#7836)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 01:55:57 +08:00
1 parent 183e2e2d1e
commit 0747e46401
4 files changed
+104 -19

No files matched your search

+2 -2
View File
@@ -701,7 +701,7 @@
"calculated_sha256",
"unpacked"
],
"additionalProperties": false
"additionalProperties": {}
},
{
"type": "object",
@@ -727,7 +727,7 @@
"calculated_md5",
"unpacked"
],
"additionalProperties": false
"additionalProperties": {}
}
]
}
+20 -16
View File
@@ -51,22 +51,26 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
code: z.literal("artifact_integrity_mismatch"),
...common,
details: z.union([
z.strictObject({
logical_path: z.string(),
declared_sha256: z.string().nullable(),
calculated_sha256: z.string().nullable(),
unpacked: z.boolean(),
}),
z.strictObject({
logical_path: z.string(),
algorithm: z.literal("md5"),
declared_md5: z.string().regex(/^[a-f0-9]{32}$/u),
calculated_md5: z
.string()
.regex(/^[a-f0-9]{32}$/u)
.nullable(),
unpacked: z.boolean(),
}),
z
.object({
logical_path: z.string(),
declared_sha256: z.string().nullable(),
calculated_sha256: z.string().nullable(),
unpacked: z.boolean(),
})
.catchall(jsonValueSchema),
z
.object({
logical_path: z.string(),
algorithm: z.literal("md5"),
declared_md5: z.string().regex(/^[a-f0-9]{32}$/u),
calculated_md5: z
.string()
.regex(/^[a-f0-9]{32}$/u)
.nullable(),
unpacked: z.boolean(),
})
.catchall(jsonValueSchema),
]),
})
.strict(),
+47
View File
@@ -89,3 +89,50 @@ cliTest(
expect(response.structuredContent).toEqual(expected);
},
);
cliTest(
"returns a failing outcome when the selected source digest disagrees",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-module-integrity-");
const artifacts = webModuleArtifactsFixture("export const a = 1;");
const files = join(root, "files");
await mkdir(join(files, "modules"), { recursive: true });
await writeFile(join(files, "modules", "main.js"), "export const a = 2;");
const manifest = join(root, "manifest.json");
await writeFile(
manifest,
JSON.stringify({
...artifacts.manifest,
output_directory: root,
analysis_input: { input_path: files, format: "directory" },
}),
);
for (const full of [false, true]) {
const result = await cli.run({
arguments: [
"trace-web-module-imports",
manifest,
"0",
"--json",
...(full ? ["--full-output"] : []),
],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(1);
const failure = {
code: "artifact_integrity_mismatch",
details: {
partial_observation: {
kind: "web-artifact-acquisition",
analysis: "not-performed",
},
},
};
if (full)
expect(result.json).toMatchObject({
ok: false,
error: failure,
});
else expect(result.json).toMatchObject(failure);
}
},
);
@@ -1,7 +1,11 @@
import { describe, expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import { ProcessCaptureError } from "../../../src/process/capture/ProcessCaptureError.js";
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
import {
analysisErrorJsonSchema,
analysisErrorProjectionSchema,
} from "../../../src/contracts/errorSchemas.js";
import { ArtifactOperationError } from "../../../src/domain/artifactOperationError.js";
import { EvidenceFileError } from "../../../src/domain/evidenceErrors.js";
import { HopperRemoteError } from "../../../src/domain/hopperErrors.js";
@@ -14,6 +18,36 @@ import {
} from "../../../src/domain/analysisErrorCore.js";
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
it.each([
{ declaredSha256: "a".repeat(64), calculatedSha256: "b".repeat(64) },
{
algorithm: "md5" as const,
declaredMd5: "a".repeat(32),
calculatedMd5: "b".repeat(32),
},
])("retains partial observations on integrity errors", (digests) => {
const error = new ArtifactOperationError(
"trace_web_module_imports",
"integrity",
{
logicalPath: "modules/main.js",
unpacked: false,
...digests,
},
);
error.retainPartialObservation({
kind: "web-artifact-acquisition",
analysis: "not-performed",
files: [],
});
const projected = projectAnalysisError(error);
expect(analysisErrorProjectionSchema.parse(projected)).toEqual(projected);
const validate = new Ajv2020({ strict: false }).compile(
analysisErrorJsonSchema,
);
expect(validate(projected), JSON.stringify(validate.errors)).toBe(true);
});
describe("analysis error projection contract", () => {
it("keeps cleanup failure distinct from unreadable collected output", () => {
const projected = projectAnalysisError(