mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(cli): recognize integrity failures with partial observations (#7836)
This commit is contained in:
1 parent
183e2e2d1e
commit
0747e46401
4 files changed
+104
-19
No files matched your search
@@ -701,7 +701,7 @@
|
||||
"calculated_sha256",
|
||||
"unpacked"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"additionalProperties": {}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
@@ -727,7 +727,7 @@
|
||||
"calculated_md5",
|
||||
"unpacked"
|
||||
],
|
||||
"additionalProperties": false
|
||||
"additionalProperties": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user