mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(evidence): validate process captures before bundle export
Reuse the read-side capture validation before publication so exported bundles can be imported. Preserve historical and foreign record identity and existing destination files on failure.
This commit is contained in:
1 parent
50717c39b6
commit
ed6668b5bc
2 files changed
+115
-2
No files matched your search
@@ -114,7 +114,13 @@ export const authenticateEvidenceBundleValue = (
|
||||
}),
|
||||
);
|
||||
}
|
||||
const bundle = parsed.value;
|
||||
return validateBundlePayloads(parsed.value);
|
||||
};
|
||||
|
||||
/** Validate typed payloads without restricting historical workflow or provider identities. */
|
||||
const validateBundlePayloads = (
|
||||
bundle: EvidenceBundle,
|
||||
): Result<EvidenceBundle, EvidenceIntegrityError> => {
|
||||
for (const record of bundle.records) {
|
||||
if (record.predicate_type !== "rea.process-capture") continue;
|
||||
const parsedCapture = safeParseProcessCapture(record.normalized_result);
|
||||
@@ -158,8 +164,10 @@ export const writeEvidenceBundle = async (
|
||||
}),
|
||||
);
|
||||
}
|
||||
const validated = validateBundlePayloads(checked.value);
|
||||
if (!validated.ok) return validated;
|
||||
return writeTextParts(
|
||||
bufferedJsonParts(canonicalJsonParts(checked.value)),
|
||||
bufferedJsonParts(canonicalJsonParts(validated.value)),
|
||||
path,
|
||||
overwrite,
|
||||
resources,
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
} from "../../../src/domain/evidenceBundle.js";
|
||||
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
|
||||
import { publishCliAnalysisEvidence } from "../../../src/cli/analysisEvidenceDelivery.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js";
|
||||
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
|
||||
|
||||
const bundle = (result = true) =>
|
||||
createEvidenceBundle([
|
||||
@@ -754,6 +756,109 @@ it.skipIf(process.platform === "win32")(
|
||||
},
|
||||
);
|
||||
|
||||
describe("evidence bundle process-capture payloads", () => {
|
||||
it.each([false, true])(
|
||||
"rejects semantically invalid capture export before publication (overwrite: %s)",
|
||||
async (overwrite) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-invalid-capture-bundle-",
|
||||
);
|
||||
const path = join(directory, "bundle.json");
|
||||
if (overwrite) await writeFile(path, "original");
|
||||
// Authenticate the enclosing Evidence correctly while breaking its
|
||||
// capture commitment, so generic bundle authentication still succeeds.
|
||||
const capture = EMPTY_PROCESS_CAPTURE_EXAMPLE;
|
||||
const record = createEvidence(undefined, PROCESS_PROVIDER, {
|
||||
predicateType: "rea.process-capture",
|
||||
operation: "capture_process_scenario",
|
||||
parameters: {},
|
||||
result: {
|
||||
...capture,
|
||||
manifest: {
|
||||
...capture.manifest,
|
||||
full_scenario_sha256: "0".repeat(64),
|
||||
},
|
||||
},
|
||||
});
|
||||
const value = createEvidenceBundle([record]);
|
||||
const rejected = await writeBundle(value, path, overwrite);
|
||||
if (rejected.ok) throw new Error("Expected invalid capture rejection");
|
||||
expect(rejected.error).toMatchObject({
|
||||
_tag: "EvidenceIntegrityError",
|
||||
userMessage: expect.stringContaining(
|
||||
`Process capture record ${record.evidence_id} has an invalid normalized_result`,
|
||||
),
|
||||
});
|
||||
expect(await readdir(directory)).toEqual(
|
||||
overwrite ? ["bundle.json"] : [],
|
||||
);
|
||||
if (overwrite) expect(await readFile(path, "utf8")).toBe("original");
|
||||
// Existing files and inline authentication use the same rejection facts.
|
||||
await writeFile(path, serializeEvidenceBundle(value));
|
||||
for (const read of [readEvidenceBundle, readImmutableEvidenceBundle]) {
|
||||
const loaded = await read(path);
|
||||
if (loaded.ok) throw new Error("Expected invalid capture rejection");
|
||||
expect(projectAnalysisError(loaded.error)).toEqual(
|
||||
projectAnalysisError(rejected.error),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "current provider",
|
||||
provider: PROCESS_PROVIDER,
|
||||
operation: "capture_process_scenario",
|
||||
predicateType: "rea.process-capture",
|
||||
result: EMPTY_PROCESS_CAPTURE_EXAMPLE,
|
||||
},
|
||||
{
|
||||
label: "historical provider name",
|
||||
provider: {
|
||||
...PROCESS_PROVIDER,
|
||||
name: "REA deterministic process harness",
|
||||
},
|
||||
operation: "capture_process_scenario",
|
||||
predicateType: "rea.process-capture",
|
||||
result: EMPTY_PROCESS_CAPTURE_EXAMPLE,
|
||||
},
|
||||
{
|
||||
label: "foreign provider and operation",
|
||||
provider: { id: "foreign", name: "Foreign capture", version: "1" },
|
||||
operation: "historical_capture",
|
||||
predicateType: "rea.process-capture",
|
||||
result: EMPTY_PROCESS_CAPTURE_EXAMPLE,
|
||||
},
|
||||
{
|
||||
label: "unrelated predicate",
|
||||
provider: PROCESS_PROVIDER,
|
||||
operation: "capture_process_scenario",
|
||||
predicateType: "foreign.payload",
|
||||
result: {},
|
||||
},
|
||||
])(
|
||||
"round trips capture-related records without narrowing their identities ($label)",
|
||||
async ({ provider, operation, predicateType, result }) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-capture-bundle-identity-",
|
||||
);
|
||||
const path = join(directory, "bundle.json");
|
||||
const value = createEvidenceBundle([
|
||||
createEvidence(undefined, provider, {
|
||||
operation,
|
||||
predicateType,
|
||||
result,
|
||||
parameters: {},
|
||||
}),
|
||||
]);
|
||||
expect(await writeBundle(value, path, false)).toMatchObject({ ok: true });
|
||||
for (const read of [readEvidenceBundle, readImmutableEvidenceBundle])
|
||||
expect(await read(path)).toEqual({ ok: true, value });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("evidence bundle filesystem adapter", () => {
|
||||
it.skipIf(process.platform === "win32").each(["/", "/."])(
|
||||
"preserves the directory requirement of a missing output ending in %s",
|
||||
|
||||
Reference in new issue
Block a user