mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(ledger): authenticate evidence before committing
This commit is contained in:
@@ -4,14 +4,16 @@ import { z } from "zod";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
createImmutableEvidenceBundle,
|
||||
safeParseEvidenceBundle,
|
||||
safeParseImmutableEvidenceBundle,
|
||||
InvalidResidualUnknownGraphError,
|
||||
validateResidualUnknownAddition,
|
||||
type EvidenceBundle,
|
||||
} from "../../domain/evidenceBundle.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { UnknownRegistryError } from "../../domain/unknownRegistryError.js";
|
||||
import { immutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { readImmutableEvidence } from "../../domain/evidenceValidation.js";
|
||||
import {
|
||||
createResidualUnknown,
|
||||
updateResidualUnknown,
|
||||
@@ -43,21 +45,17 @@ type ImportResult = Result<
|
||||
|
||||
/** Session-owned set of immutable evidence records. */
|
||||
export class EvidenceLedger {
|
||||
readonly #records = new Map<string, Evidence>();
|
||||
readonly #unknownRevisions = new Map<string, ResidualUnknown>();
|
||||
readonly #unknownHeads = new Map<string, ResidualUnknown>();
|
||||
#records = new Map<string, Evidence>();
|
||||
#unknownRevisions = new Map<string, ResidualUnknown>();
|
||||
#unknownHeads = new Map<string, ResidualUnknown>();
|
||||
/** Record evidence idempotently; conflicting content is rejected. */
|
||||
record(input: Evidence): RecordResult {
|
||||
let evidence: Evidence;
|
||||
try {
|
||||
evidence = immutableEvidence(input);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new EvidenceIntegrityError("Evidence record validation failed", {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const authenticated = readLedgerEvidence(
|
||||
input,
|
||||
"Evidence record validation failed",
|
||||
);
|
||||
if (!authenticated.ok) return authenticated;
|
||||
const evidence = authenticated.value;
|
||||
const existing = this.#records.get(evidence.evidence_id);
|
||||
if (existing !== undefined) {
|
||||
// A borrowed record is already authenticated and deeply immutable. Its
|
||||
@@ -74,7 +72,7 @@ export class EvidenceLedger {
|
||||
|
||||
/** Validate an entire bundle before atomically merging its records. */
|
||||
import(input: unknown): ImportResult {
|
||||
const parsed = safeParseEvidenceBundle(input);
|
||||
const parsed = safeParseImmutableEvidenceBundle(input);
|
||||
if (!parsed.ok) {
|
||||
const { kind, message, cause } = parsed.error;
|
||||
if (kind === "single-record")
|
||||
@@ -116,9 +114,8 @@ export class EvidenceLedger {
|
||||
additions.set(evidence.evidence_id, evidence);
|
||||
}
|
||||
for (const [id, evidence] of replacements)
|
||||
this.#records.set(id, immutableEvidence(evidence));
|
||||
for (const [id, evidence] of additions)
|
||||
this.#records.set(id, immutableEvidence(evidence));
|
||||
this.#records.set(id, evidence);
|
||||
for (const [id, evidence] of additions) this.#records.set(id, evidence);
|
||||
return ok({
|
||||
recordsAdded: additions.size,
|
||||
unknownsAdded: 0,
|
||||
@@ -154,19 +151,14 @@ export class EvidenceLedger {
|
||||
}
|
||||
const checked = this.#validateCandidate(pending, pendingUnknowns);
|
||||
if (!checked.ok) {
|
||||
if (checked.error instanceof UnknownRegistryError)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Evidence bundle unknown registry failed",
|
||||
{
|
||||
cause: checked.error,
|
||||
},
|
||||
),
|
||||
);
|
||||
return err(checked.error);
|
||||
return err(
|
||||
new EvidenceIntegrityError("Evidence bundle unknown registry failed", {
|
||||
cause: checked.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const added = pending.size - this.#records.size;
|
||||
this.#commit(pending, pendingUnknowns);
|
||||
this.#commit(checked.value);
|
||||
return ok({
|
||||
recordsAdded: added,
|
||||
unknownsAdded,
|
||||
@@ -217,16 +209,12 @@ export class EvidenceLedger {
|
||||
ResidualUnknown,
|
||||
EvidenceIntegrityError | UnknownRegistryError | AnalysisInputError
|
||||
> {
|
||||
let parsedMutation: Evidence;
|
||||
try {
|
||||
parsedMutation = immutableEvidence(mutationEvidence);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new EvidenceIntegrityError("Unknown mutation evidence is invalid", {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const authenticated = readLedgerEvidence(
|
||||
mutationEvidence,
|
||||
"Unknown mutation evidence is invalid",
|
||||
);
|
||||
if (!authenticated.ok) return authenticated;
|
||||
const parsedMutation = authenticated.value;
|
||||
let unknown: ResidualUnknown;
|
||||
try {
|
||||
unknown = createResidualUnknown(
|
||||
@@ -235,7 +223,8 @@ export class EvidenceLedger {
|
||||
parsedMutation.subject?.digest.sha256 ?? null,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(unknownInputError("record_unknown", cause, input));
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(requestInputError("record_unknown", cause, input));
|
||||
}
|
||||
if (this.#unknownHeads.has(unknown.unknown_id))
|
||||
return err(new UnknownRegistryError("already-exists"));
|
||||
@@ -251,18 +240,18 @@ export class EvidenceLedger {
|
||||
ResidualUnknown | null,
|
||||
EvidenceIntegrityError | UnknownRegistryError
|
||||
> {
|
||||
let evidence: Evidence;
|
||||
let mutation: Evidence;
|
||||
try {
|
||||
evidence = immutableEvidence(evidenceInput);
|
||||
mutation = immutableEvidence(mutationInput);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new EvidenceIntegrityError("Atomic Evidence validation failed", {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const authenticatedEvidence = readLedgerEvidence(
|
||||
evidenceInput,
|
||||
"Atomic Evidence validation failed",
|
||||
);
|
||||
if (!authenticatedEvidence.ok) return authenticatedEvidence;
|
||||
const authenticatedMutation = readLedgerEvidence(
|
||||
mutationInput,
|
||||
"Atomic Evidence validation failed",
|
||||
);
|
||||
if (!authenticatedMutation.ok) return authenticatedMutation;
|
||||
const evidence = authenticatedEvidence.value;
|
||||
const mutation = authenticatedMutation.value;
|
||||
const existingEvidence = this.#records.get(evidence.evidence_id);
|
||||
if (
|
||||
existingEvidence !== undefined &&
|
||||
@@ -277,6 +266,7 @@ export class EvidenceLedger {
|
||||
mutation.subject?.digest.sha256 ?? null,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(new UnknownRegistryError("invalid-transition", { cause }));
|
||||
}
|
||||
const creationUnknown = this.#unknownRevisions.get(
|
||||
@@ -315,9 +305,15 @@ export class EvidenceLedger {
|
||||
mutationEvidence.evidence_id,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(unknownInputError("update_unknown", cause, input));
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(requestInputError("update_unknown", cause, input));
|
||||
}
|
||||
return this.#appendUnknown(unknown, mutationEvidence);
|
||||
const authenticated = readLedgerEvidence(
|
||||
mutationEvidence,
|
||||
"Unknown mutation evidence is invalid",
|
||||
);
|
||||
if (!authenticated.ok) return authenticated;
|
||||
return this.#appendUnknown(unknown, authenticated.value);
|
||||
}
|
||||
|
||||
/** Query current heads in stable ID order. */
|
||||
@@ -391,23 +387,14 @@ export class EvidenceLedger {
|
||||
|
||||
#appendUnknown(
|
||||
unknown: ResidualUnknown,
|
||||
mutationEvidenceInput: Evidence,
|
||||
mutationEvidence: Evidence,
|
||||
): Result<ResidualUnknown, EvidenceIntegrityError | UnknownRegistryError> {
|
||||
let mutationEvidence: Evidence;
|
||||
try {
|
||||
mutationEvidence = immutableEvidence(mutationEvidenceInput);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new EvidenceIntegrityError("Unknown mutation evidence is invalid", {
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const checked = this.#appendIncremental([mutationEvidence], unknown);
|
||||
if (!checked.ok) return checked;
|
||||
return ok(structuredClone(unknown));
|
||||
}
|
||||
|
||||
/** Incoming records were authenticated and sealed at the ingestion boundary. */
|
||||
#appendIncremental(
|
||||
records: readonly Evidence[],
|
||||
unknown?: ResidualUnknown,
|
||||
@@ -439,10 +426,10 @@ export class EvidenceLedger {
|
||||
this.#unknownHeads,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof InvalidResidualUnknownGraphError)) throw cause;
|
||||
return err(new UnknownRegistryError("integrity", { cause }));
|
||||
}
|
||||
for (const [id, evidence] of additions)
|
||||
this.#records.set(id, immutableEvidence(evidence));
|
||||
for (const [id, evidence] of additions) this.#records.set(id, evidence);
|
||||
if (unknown !== undefined && unknownKey !== undefined) {
|
||||
this.#unknownRevisions.set(unknownKey, unknown);
|
||||
this.#unknownHeads.set(unknown.unknown_id, unknown);
|
||||
@@ -450,48 +437,60 @@ export class EvidenceLedger {
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
/** Return the sealed candidate facts that the commit will adopt. */
|
||||
#validateCandidate(
|
||||
records: ReadonlyMap<string, Evidence>,
|
||||
unknowns: ReadonlyMap<string, ResidualUnknown>,
|
||||
): Result<void, EvidenceIntegrityError | UnknownRegistryError> {
|
||||
): Result<EvidenceBundle, UnknownRegistryError> {
|
||||
try {
|
||||
createImmutableEvidenceBundle(
|
||||
[...records.values()],
|
||||
[...unknowns.values()],
|
||||
return ok(
|
||||
createImmutableEvidenceBundle(
|
||||
[...records.values()],
|
||||
[...unknowns.values()],
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
!(cause instanceof z.ZodError) &&
|
||||
!(cause instanceof InvalidResidualUnknownGraphError)
|
||||
)
|
||||
throw cause;
|
||||
return err(new UnknownRegistryError("integrity", { cause }));
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
#commit(
|
||||
records: ReadonlyMap<string, Evidence>,
|
||||
unknowns: ReadonlyMap<string, ResidualUnknown>,
|
||||
): void {
|
||||
this.#records.clear();
|
||||
for (const [id, evidence] of records)
|
||||
this.#records.set(id, immutableEvidence(evidence));
|
||||
this.#unknownRevisions.clear();
|
||||
this.#unknownHeads.clear();
|
||||
for (const [key, unknown] of unknowns) {
|
||||
this.#unknownRevisions.set(key, unknown);
|
||||
const head = this.#unknownHeads.get(unknown.unknown_id);
|
||||
#commit(bundle: EvidenceBundle): void {
|
||||
const records = new Map(
|
||||
bundle.records.map((evidence) => [evidence.evidence_id, evidence]),
|
||||
);
|
||||
const unknownRevisions = new Map(
|
||||
bundle.unknowns.map((unknown) => [unknownRevisionKey(unknown), unknown]),
|
||||
);
|
||||
const unknownHeads = new Map<string, ResidualUnknown>();
|
||||
for (const unknown of bundle.unknowns) {
|
||||
const head = unknownHeads.get(unknown.unknown_id);
|
||||
if (head === undefined || head.revision < unknown.revision)
|
||||
this.#unknownHeads.set(unknown.unknown_id, unknown);
|
||||
unknownHeads.set(unknown.unknown_id, unknown);
|
||||
}
|
||||
this.#records = records;
|
||||
this.#unknownRevisions = unknownRevisions;
|
||||
this.#unknownHeads = unknownHeads;
|
||||
}
|
||||
}
|
||||
|
||||
/** A caller's unknown that fails its schema is an input mistake, not a state change. */
|
||||
const unknownInputError = (
|
||||
operation: "record_unknown" | "update_unknown",
|
||||
cause: unknown,
|
||||
input: RecordUnknownInput | UpdateUnknownInput,
|
||||
): UnknownRegistryError | AnalysisInputError =>
|
||||
cause instanceof z.ZodError
|
||||
? requestInputError(operation, cause, input)
|
||||
: new UnknownRegistryError("invalid-transition", { cause });
|
||||
const readLedgerEvidence = (
|
||||
input: unknown,
|
||||
message: string,
|
||||
): Result<Evidence, EvidenceIntegrityError> => {
|
||||
const checked = readImmutableEvidence(input);
|
||||
return checked.ok
|
||||
? checked
|
||||
: err(
|
||||
new EvidenceIntegrityError(message, {
|
||||
cause: checked.error.cause,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const unknownRevisionKey = (unknown: ResidualUnknown): string =>
|
||||
`${unknown.unknown_id}:${String(unknown.revision)}`;
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import * as evidenceDomain from "../../src/domain/evidence.js";
|
||||
import * as evidenceBundleDomain from "../../src/domain/evidenceBundle.js";
|
||||
import * as residualUnknownDomain from "../../src/domain/residualUnknown.js";
|
||||
import {
|
||||
recordUnknownInputSchema,
|
||||
updateUnknownInputSchema,
|
||||
} from "../../src/domain/residualUnknown.js";
|
||||
import { processEvidence } from "../../src/application/ReconstructionObligationLedger.fixture.js";
|
||||
import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js";
|
||||
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
||||
import { InvestigationRecords } from "../../src/application/investigation/InvestigationRecords.js";
|
||||
|
||||
type ApplicationClient = Awaited<
|
||||
ReturnType<typeof createApplicationMcpHarness>
|
||||
>["client"];
|
||||
|
||||
const unknownInput = (question: string) =>
|
||||
recordUnknownInputSchema.parse({
|
||||
question,
|
||||
domain: "ledger-failure",
|
||||
severity: "medium",
|
||||
required_authority: "analyst-inference",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
});
|
||||
|
||||
const updateInput = (unknownId: string, expectedRevision: number) =>
|
||||
updateUnknownInputSchema.parse({
|
||||
unknown_id: unknownId,
|
||||
expected_revision: expectedRevision,
|
||||
status: "investigating",
|
||||
severity: "medium",
|
||||
supporting_evidence_ids: [],
|
||||
contradicting_evidence_ids: [],
|
||||
required_authority: "analyst-inference",
|
||||
required_confidence: "observed",
|
||||
required_environment: null,
|
||||
recommended_probes: [],
|
||||
relationships: [],
|
||||
resolution: null,
|
||||
});
|
||||
|
||||
const failureFor = (kind: "TypeError" | "Error", message: string): Error =>
|
||||
kind === "TypeError" ? new TypeError(message) : new Error(message);
|
||||
|
||||
const expectSdkFailure = async (
|
||||
client: ApplicationClient,
|
||||
name: "record_unknown" | "update_unknown",
|
||||
args: Record<string, unknown>,
|
||||
failure: Error,
|
||||
): Promise<void> => {
|
||||
const response = await client.callTool({
|
||||
name,
|
||||
arguments: args,
|
||||
});
|
||||
expect(response).toMatchObject({
|
||||
isError: true,
|
||||
content: [{ type: "text", text: failure.message }],
|
||||
});
|
||||
expect(response.structuredContent).toBeUndefined();
|
||||
};
|
||||
|
||||
const withHarness = async <T>(
|
||||
run: (
|
||||
harness: Awaited<ReturnType<typeof createApplicationMcpHarness>>,
|
||||
) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Ledger mutations must not launch a provider");
|
||||
});
|
||||
const harness = await createApplicationMcpHarness({ session });
|
||||
try {
|
||||
return await run(harness);
|
||||
} finally {
|
||||
vi.restoreAllMocks();
|
||||
await harness.close();
|
||||
}
|
||||
};
|
||||
|
||||
it.each(
|
||||
(["recordEvidence", "recordEvidenceWithUnknown"] as const).flatMap(
|
||||
(operation) =>
|
||||
(["TypeError", "Error"] as const).map(
|
||||
(kind) => [operation, kind] as const,
|
||||
),
|
||||
),
|
||||
)(
|
||||
"lets unexpected authentication failure escape %s (%s) without changing the bundle",
|
||||
async (operation, kind) =>
|
||||
withHarness(async ({ session }) => {
|
||||
const derived = processEvidence();
|
||||
const input = unknownInput(`Failure during ${operation}`);
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = failureFor(kind, `${operation} authentication failed`);
|
||||
const authenticate = vi
|
||||
.spyOn(evidenceDomain, "immutableEvidence")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => authenticate.mockRestore());
|
||||
|
||||
if (operation === "recordEvidence")
|
||||
expect(() => session.recordEvidence(derived)).toThrow(failure);
|
||||
else
|
||||
expect(() => session.recordEvidenceWithUnknown(derived, input)).toThrow(
|
||||
failure,
|
||||
);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}),
|
||||
);
|
||||
|
||||
it("preserves an unexpected unknown-construction fault through record_unknown", async () =>
|
||||
withHarness(async ({ client, session }) => {
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = new TypeError("unknown constructor implementation failed");
|
||||
const construct = vi
|
||||
.spyOn(residualUnknownDomain, "createResidualUnknown")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => construct.mockRestore());
|
||||
|
||||
await expectSdkFailure(
|
||||
client,
|
||||
"record_unknown",
|
||||
unknownInput("Unexpected constructor failure"),
|
||||
failure,
|
||||
);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}));
|
||||
|
||||
it("preserves an unexpected unknown-revision fault through update_unknown", async () =>
|
||||
withHarness(async ({ client, session }) => {
|
||||
const created = session.recordUnknown(unknownInput("Update failure setup"));
|
||||
if (!created.ok) throw created.error;
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = new Error("unknown update implementation failed");
|
||||
const update = vi
|
||||
.spyOn(residualUnknownDomain, "updateResidualUnknown")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => update.mockRestore());
|
||||
|
||||
await expectSdkFailure(
|
||||
client,
|
||||
"update_unknown",
|
||||
updateInput(created.value.unknown_id, created.value.revision),
|
||||
failure,
|
||||
);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}));
|
||||
|
||||
it.each(
|
||||
(["record_unknown", "update_unknown"] as const).flatMap((operation) =>
|
||||
(["TypeError", "Error"] as const).map((kind) => [operation, kind] as const),
|
||||
),
|
||||
)(
|
||||
"preserves unexpected mutation Evidence authentication failure through %s (%s)",
|
||||
async (operation, kind) =>
|
||||
withHarness(async ({ client, session }) => {
|
||||
let args: Record<string, unknown>;
|
||||
if (operation === "record_unknown") {
|
||||
args = unknownInput("Mutation Evidence authentication failure");
|
||||
} else {
|
||||
const created = session.recordUnknown(
|
||||
unknownInput("Mutation Evidence authentication setup"),
|
||||
);
|
||||
if (!created.ok) throw created.error;
|
||||
args = updateInput(created.value.unknown_id, created.value.revision);
|
||||
}
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = failureFor(
|
||||
kind,
|
||||
"mutation Evidence authentication failed",
|
||||
);
|
||||
const authenticate = vi
|
||||
.spyOn(evidenceDomain, "immutableEvidence")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => authenticate.mockRestore());
|
||||
|
||||
await expectSdkFailure(client, operation, args, failure);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}),
|
||||
);
|
||||
|
||||
it.each([
|
||||
["record_unknown", "record graph failure"],
|
||||
["update_unknown", "update graph failure"],
|
||||
] as const)(
|
||||
"does not convert an unexpected graph-validation fault during %s into a caller error",
|
||||
async (operation, message) =>
|
||||
withHarness(async ({ client, session }) => {
|
||||
let args: Record<string, unknown>;
|
||||
if (operation === "record_unknown") {
|
||||
args = unknownInput(`Unexpected graph validation: ${message}`);
|
||||
} else {
|
||||
const created = session.recordUnknown(
|
||||
unknownInput(`Graph validation setup: ${message}`),
|
||||
);
|
||||
if (!created.ok) throw created.error;
|
||||
args = updateInput(created.value.unknown_id, created.value.revision);
|
||||
}
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = new Error(message);
|
||||
const validate = vi
|
||||
.spyOn(evidenceBundleDomain, "validateResidualUnknownAddition")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => validate.mockRestore());
|
||||
|
||||
await expectSdkFailure(client, operation, args, failure);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}),
|
||||
);
|
||||
|
||||
it("preserves unexpected full-candidate bundle validation faults and leaves import state unchanged", async () =>
|
||||
withHarness(async ({ session }) => {
|
||||
const source = new InvestigationRecords();
|
||||
const created = source.recordUnknown(unknownInput("Imported history"));
|
||||
if (!created.ok) throw created.error;
|
||||
const bundle = source.exportEvidenceBundle();
|
||||
const before = session.exportEvidenceBundle();
|
||||
const failure = new TypeError("candidate bundle validation failed");
|
||||
const validate = vi
|
||||
.spyOn(evidenceBundleDomain, "createImmutableEvidenceBundle")
|
||||
.mockImplementation(() => {
|
||||
throw failure;
|
||||
});
|
||||
onTestFinished(() => validate.mockRestore());
|
||||
|
||||
expect(() => session.importEvidenceBundle(bundle)).toThrow(failure);
|
||||
expect(session.exportEvidenceBundle()).toEqual(before);
|
||||
}));
|
||||
|
||||
it("authenticates derived and mutation Evidence before committing recordWithUnknown", async () =>
|
||||
withHarness(async ({ session }) => {
|
||||
const input = unknownInput("Atomic record with unknown");
|
||||
const evidence = processEvidence();
|
||||
const expectedId = evidence.evidence_id;
|
||||
const original = evidenceDomain.immutableEvidence;
|
||||
let evidenceVisibleDuringAuthentication = false;
|
||||
const authenticate = vi
|
||||
.spyOn(evidenceDomain, "immutableEvidence")
|
||||
.mockImplementation((candidate) => {
|
||||
if (
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.some((record) => record.evidence_id === expectedId)
|
||||
)
|
||||
evidenceVisibleDuringAuthentication = true;
|
||||
return original(candidate);
|
||||
});
|
||||
onTestFinished(() => authenticate.mockRestore());
|
||||
|
||||
const recorded = session.recordEvidenceWithUnknown(evidence, input);
|
||||
authenticate.mockRestore();
|
||||
expect(recorded.ok).toBe(true);
|
||||
expect(evidenceVisibleDuringAuthentication).toBe(false);
|
||||
const committed = session.exportEvidenceBundle();
|
||||
expect(committed.records.map(({ evidence_id }) => evidence_id)).toContain(
|
||||
expectedId,
|
||||
);
|
||||
expect(committed.unknowns).toHaveLength(1);
|
||||
expect(committed.records).toHaveLength(2);
|
||||
expect(
|
||||
session.immutableEvidenceBundle().records.every(Object.isFrozen),
|
||||
).toBe(true);
|
||||
}));
|
||||
|
||||
it("authenticates all Evidence before committing an imported unknown history", async () =>
|
||||
withHarness(async ({ session }) => {
|
||||
const source = new InvestigationRecords();
|
||||
expect(source.recordEvidence(processEvidence()).ok).toBe(true);
|
||||
const created = source.recordUnknown(
|
||||
unknownInput("Atomic imported history"),
|
||||
);
|
||||
if (!created.ok) throw created.error;
|
||||
const bundle = source.exportEvidenceBundle();
|
||||
const expectedIds = bundle.records.map(({ evidence_id }) => evidence_id);
|
||||
const original = evidenceDomain.immutableEvidence;
|
||||
let incomingVisibleDuringAuthentication = false;
|
||||
const authenticate = vi
|
||||
.spyOn(evidenceDomain, "immutableEvidence")
|
||||
.mockImplementation((input) => {
|
||||
const visible = session
|
||||
.exportEvidenceBundle()
|
||||
.records.some(({ evidence_id }) => expectedIds.includes(evidence_id));
|
||||
if (visible) incomingVisibleDuringAuthentication = true;
|
||||
return original(input);
|
||||
});
|
||||
onTestFinished(() => authenticate.mockRestore());
|
||||
|
||||
const imported = session.importEvidenceBundle(bundle);
|
||||
authenticate.mockRestore();
|
||||
expect(imported.ok).toBe(true);
|
||||
expect(incomingVisibleDuringAuthentication).toBe(false);
|
||||
expect(session.exportEvidenceBundle()).toEqual(bundle);
|
||||
expect(
|
||||
session.immutableEvidenceBundle().records.every(Object.isFrozen),
|
||||
).toBe(true);
|
||||
}));
|
||||
Reference in New Issue
Block a user