fix(analysis-view): preserve artifact parent schema diagnostics (#6361)
* fix(analysis-view): preserve artifact parent schema diagnostics * fix(artifacts): locate nested Evidence validation failures
This commit is contained in:
1 parent
9a426033ff
commit
295e7efddf
4 files changed
+195
-7
No files matched your search
@@ -35,6 +35,7 @@ import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewResultInputError } from "./analysisViewParent.js";
|
||||
|
||||
const page = {
|
||||
kind: z.literal("inventory"),
|
||||
@@ -172,7 +173,7 @@ const parseSource = (
|
||||
cached.source.inventory.manifest,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return sourceFailure(cause);
|
||||
return sourceFailure(parent.normalizedResult, cause);
|
||||
}
|
||||
return ok(cached.source);
|
||||
}
|
||||
@@ -208,13 +209,17 @@ const parseSourceUncached = (
|
||||
inspection: parsed.inspection,
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
return sourceFailure(cause);
|
||||
return sourceFailure(parent.normalizedResult, cause);
|
||||
}
|
||||
};
|
||||
|
||||
const sourceFailure = (cause: unknown): Result<never, AnalysisError> => {
|
||||
if (!(cause instanceof TypeError) && !(cause instanceof z.ZodError))
|
||||
throw cause;
|
||||
const sourceFailure = (
|
||||
input: unknown,
|
||||
cause: unknown,
|
||||
): Result<never, AnalysisError> => {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(analysisViewResultInputError(input, cause));
|
||||
if (!(cause instanceof TypeError)) throw cause;
|
||||
return err(inputError(cause.message));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { freezeJsonSnapshot } from "./immutableJson.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js";
|
||||
import { createArtifactInspection } from "./artifactInspection.js";
|
||||
@@ -86,3 +87,28 @@ it("retains the authenticated parent's nested facts without a second inventory c
|
||||
owned.substeps[0]!.evidence.normalized_result.nodes[0],
|
||||
);
|
||||
});
|
||||
|
||||
it.each([0, 1])(
|
||||
"locates a stale nested Evidence ID at substep %i and retains its cause",
|
||||
(index) => {
|
||||
const input = structuredClone(inspection);
|
||||
input.substeps.push(structuredClone(input.substeps[0]!));
|
||||
input.coverage.substeps_completed = input.substeps.length;
|
||||
expect(
|
||||
parseArtifactInspectionInventoryResult(input, target).inventory.complete,
|
||||
).toBe(true);
|
||||
input.substeps[index]!.evidence.evidence_id = `ev_${"f".repeat(64)}`;
|
||||
expect(() =>
|
||||
parseArtifactInspectionInventoryResult(input, target),
|
||||
).toThrowError(
|
||||
expect.objectContaining({
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["substeps", index, "evidence", "evidence_id"],
|
||||
}),
|
||||
],
|
||||
cause: expect.any(z.ZodError),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
occurrenceIdForLocation,
|
||||
} from "./artifactIdentity.js";
|
||||
import { parseEvidence, type Evidence } from "./evidence.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
artifactInspectionResultSchema,
|
||||
type ArtifactInspectionResult,
|
||||
@@ -124,7 +125,21 @@ const parseInspectionInventoryPayload = (
|
||||
);
|
||||
const sourceEvidence =
|
||||
immutableJsonSnapshotMember(sealedStep, "evidence") ?? step.evidence;
|
||||
const parsedEvidence = parseEvidence(sourceEvidence);
|
||||
let parsedEvidence: Evidence;
|
||||
try {
|
||||
parsedEvidence = parseEvidence(sourceEvidence);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
throw Object.assign(
|
||||
new z.ZodError(
|
||||
cause.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: ["substeps", index, "evidence", ...issue.path],
|
||||
})),
|
||||
),
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
const evidence = canBorrowParsedJsonSnapshot(sourceEvidence, parsedEvidence)
|
||||
? sourceEvidence
|
||||
: parsedEvidence;
|
||||
|
||||
@@ -29,7 +29,14 @@ import {
|
||||
environmentManifestCaptureFixture,
|
||||
environmentStateCaptureFixture,
|
||||
} from "../../fixtures/analysisView.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../../src/domain/evidence.js";
|
||||
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
import { z } from "zod";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
|
||||
@@ -294,6 +301,141 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves artifact parent schema diagnostics through the application and CLI",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-inventory-diagnostics-cli-",
|
||||
);
|
||||
const archive = join(root, "fixture.zip");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("main.js", new TextReader("console.log(1);\n"));
|
||||
await writeFile(archive, await writer.close());
|
||||
const inspected = await cli.run({
|
||||
arguments: ["inspect-artifact", archive, "--json"],
|
||||
});
|
||||
expect(inspected.exitCode).toBe(0);
|
||||
const inspection = parseEvidence(inspected.json);
|
||||
const inventory = artifactInspectionResultSchema.parse(
|
||||
inspection.normalized_result,
|
||||
).substeps[0]!.evidence;
|
||||
const path = join(root, "view.json");
|
||||
const reseal = (carrier: Evidence, result: JsonValue) => {
|
||||
const subject = carrier.subject!;
|
||||
return createEvidence(
|
||||
{
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
},
|
||||
carrier.provider,
|
||||
{ operation: carrier.operation, parameters: {}, result },
|
||||
);
|
||||
};
|
||||
for (const [carrier, group, field] of [
|
||||
[inventory, "manifest", "node_count"],
|
||||
[inspection, "coverage", "substeps_completed"],
|
||||
] as const) {
|
||||
for (const variant of ["valid", "type", "missing"] as const) {
|
||||
const payload = structuredClone(carrier.normalized_result) as Record<
|
||||
string,
|
||||
Record<string, JsonValue>
|
||||
>;
|
||||
if (variant === "type") payload[group]![field] = "bad";
|
||||
if (variant === "missing") delete payload[group]![field];
|
||||
const evidence = reseal(carrier, payload);
|
||||
const request = {
|
||||
source: { kind: "inline" as const, evidence },
|
||||
view: { kind: "summary" as const },
|
||||
};
|
||||
const application = inspectAnalysisView(request);
|
||||
await writeFile(path, JSON.stringify(request));
|
||||
const reply = await cli.run({
|
||||
arguments: ["inspect-analysis-view", path, "--json"],
|
||||
});
|
||||
expect(reply.stderr).toBe("");
|
||||
if (variant === "valid") {
|
||||
expect(application.ok).toBe(true);
|
||||
expect(reply.exitCode).toBe(0);
|
||||
expect(parseEvidence(reply.json).normalized_result).toMatchObject({
|
||||
kind: "summary",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
expect(application.ok).toBe(false);
|
||||
if (application.ok) throw new Error("Expected an invalid parent");
|
||||
expect(application.error.cause).toBeInstanceOf(z.ZodError);
|
||||
expect(reply.exitCode).toBe(1);
|
||||
expect(reply.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
retryable: false,
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "inspect_analysis_view",
|
||||
},
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["source", "normalized_result", group, field],
|
||||
reason:
|
||||
variant === "type" ? "invalid_type" : "missing_argument",
|
||||
expected: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const nested = artifactInspectionResultSchema.parse(
|
||||
inspection.normalized_result,
|
||||
);
|
||||
nested.substeps.push(structuredClone(nested.substeps[0]!));
|
||||
nested.coverage.substeps_completed = nested.substeps.length;
|
||||
nested.substeps[1]!.evidence.evidence_id = `ev_${"f".repeat(64)}`;
|
||||
const request = {
|
||||
source: { kind: "inline" as const, evidence: reseal(inspection, nested) },
|
||||
view: { kind: "summary" as const },
|
||||
};
|
||||
const application = inspectAnalysisView(request);
|
||||
expect(application.ok).toBe(false);
|
||||
if (application.ok) throw new Error("Expected invalid nested Evidence");
|
||||
expect(application.error.cause).toMatchObject({
|
||||
cause: expect.any(z.ZodError),
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["substeps", 1, "evidence", "evidence_id"],
|
||||
}),
|
||||
],
|
||||
});
|
||||
await writeFile(path, JSON.stringify(request));
|
||||
const refused = await cli.run({
|
||||
arguments: ["inspect-analysis-view", path, "--json"],
|
||||
});
|
||||
expect(refused.exitCode).toBe(1);
|
||||
expect(refused.stderr).toBe("");
|
||||
expect(refused.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: [
|
||||
"source",
|
||||
"normalized_result",
|
||||
"substeps",
|
||||
1,
|
||||
"evidence",
|
||||
"evidence_id",
|
||||
],
|
||||
reason: "invalid_value",
|
||||
message: "Evidence semantic identifier does not match its record",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"admits portable captures before parsing and preserves capture caveats",
|
||||
async ({ cli }) => {
|
||||
|
||||
Reference in new issue
Block a user