mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(snapshot): decline ambiguous exact evidence replay (#7037)
This commit is contained in:
@@ -422,6 +422,51 @@ describe("analysis snapshot canonical ordering and scope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not select arbitrary provenance when multiple authenticated records match one replay entry", () => {
|
||||
const { current, snapshot } = snapshotWithHistoricalEvidence();
|
||||
const source = snapshot.evidence_bundle.records.find(
|
||||
(record) => record.operation === "unprofiled_query",
|
||||
)!;
|
||||
const alternative = createEvidence(
|
||||
ANALYSIS_SNAPSHOT_TARGET,
|
||||
ANALYSIS_SNAPSHOT_PROVIDER,
|
||||
{
|
||||
operation: current.operation,
|
||||
parameters: current.parameters,
|
||||
result: current.normalized_result,
|
||||
analysisProfile: ANALYSIS_SNAPSHOT_PROFILE,
|
||||
evidenceLinks: [source.evidence_id],
|
||||
},
|
||||
);
|
||||
expect(alternative.evidence_id).not.toBe(current.evidence_id);
|
||||
const ambiguous = parseAnalysisSnapshot({
|
||||
...snapshot,
|
||||
evidence_bundle: createEvidenceBundle([
|
||||
...snapshot.evidence_bundle.records,
|
||||
alternative,
|
||||
]),
|
||||
});
|
||||
expect(ambiguous.entries).toEqual(snapshot.entries);
|
||||
const reversedInsertion = parseAnalysisSnapshot({
|
||||
...ambiguous,
|
||||
evidence_bundle: createEvidenceBundle(
|
||||
[...ambiguous.evidence_bundle.records].reverse(),
|
||||
),
|
||||
});
|
||||
expect(reversedInsertion).toEqual(ambiguous);
|
||||
for (const candidate of [ambiguous, reversedInsertion])
|
||||
expect(
|
||||
snapshotEvidenceForQuery(candidate, {
|
||||
target: ANALYSIS_SNAPSHOT_TARGET,
|
||||
bindingProfile: ANALYSIS_SNAPSHOT_PROFILE,
|
||||
operation: current.operation,
|
||||
parameters: current.parameters,
|
||||
provider: ANALYSIS_SNAPSHOT_PROVIDER,
|
||||
evidenceProfile: ANALYSIS_SNAPSHOT_PROFILE,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
describe("analysis snapshot Evidence binding", () => {
|
||||
it("finds only Evidence committed to the exact binding and profile", () => {
|
||||
const { current, snapshot } = snapshotWithHistoricalEvidence();
|
||||
|
||||
@@ -502,20 +502,31 @@ export const snapshotEvidenceForQuery = (
|
||||
(candidate) => candidate.query_id === queryId,
|
||||
);
|
||||
if (entry === undefined && workflowEntry === undefined) return undefined;
|
||||
const evidence = snapshot.evidence_bundle.records.find(
|
||||
(record) =>
|
||||
record.subject?.digest.sha256 === target.sha256 &&
|
||||
record.operation === operation &&
|
||||
record.provider.id === provider.id &&
|
||||
record.provider.name === provider.name &&
|
||||
record.provider.version === provider.version &&
|
||||
record.analysis_profile !== null &&
|
||||
analysisProfilesEqual(record.analysis_profile, evidenceProfile) &&
|
||||
canonicalJson(record.parameters) === encodedParameters &&
|
||||
((entry !== undefined && evidenceMatchesEntry(record, entry, snapshot)) ||
|
||||
(workflowEntry !== undefined &&
|
||||
evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot))),
|
||||
);
|
||||
let evidence: Evidence | undefined;
|
||||
for (const record of snapshot.evidence_bundle.records) {
|
||||
if (
|
||||
!(
|
||||
record.subject?.digest.sha256 === target.sha256 &&
|
||||
record.operation === operation &&
|
||||
record.provider.id === provider.id &&
|
||||
record.provider.name === provider.name &&
|
||||
record.provider.version === provider.version &&
|
||||
record.analysis_profile !== null &&
|
||||
analysisProfilesEqual(record.analysis_profile, evidenceProfile) &&
|
||||
canonicalJson(record.parameters) === encodedParameters &&
|
||||
((entry !== undefined &&
|
||||
evidenceMatchesEntry(record, entry, snapshot)) ||
|
||||
(workflowEntry !== undefined &&
|
||||
evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot)))
|
||||
)
|
||||
)
|
||||
continue;
|
||||
// Entries bind the result, not every part of the Evidence commitment
|
||||
// (for example, upstream links). Do not choose provenance by ID sort order.
|
||||
if (evidence !== undefined && evidence.evidence_id !== record.evidence_id)
|
||||
return undefined;
|
||||
evidence = record;
|
||||
}
|
||||
return evidence === undefined
|
||||
? undefined
|
||||
: rebindEvidenceToTargetPath(evidence, target);
|
||||
|
||||
@@ -6,6 +6,105 @@ import { expect, it } from "vitest";
|
||||
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { composedWorkflowFixture } from "../../fixtures/composedWorkflowEvidence.js";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
|
||||
it.each([0, 1, 12])(
|
||||
"does not replay an arbitrary historical workflow provenance when the snapshot binding is ambiguous (%i procedures)",
|
||||
async (procedureCount) => {
|
||||
const original = await composedWorkflowFixture();
|
||||
original.summary.procedure_count = procedureCount;
|
||||
const target = await original.open();
|
||||
const snapshotPath = join(dirname(original.path), "analysis.json");
|
||||
const current = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
original.dependencies,
|
||||
original.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath },
|
||||
),
|
||||
);
|
||||
expect(current.evidence_links).toHaveLength(1);
|
||||
const snapshot = parseAnalysisSnapshot(
|
||||
JSON.parse(await readFile(snapshotPath, "utf8")),
|
||||
);
|
||||
const uniqueReplay = await composedWorkflowFixture();
|
||||
expect(uniqueReplay.session.importAnalysisSnapshot(snapshot).ok).toBe(true);
|
||||
expect(uniqueReplay.session.importAnalysisSnapshot(snapshot)).toEqual(
|
||||
ok(0),
|
||||
);
|
||||
const unique = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
uniqueReplay.dependencies,
|
||||
original.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath },
|
||||
),
|
||||
);
|
||||
expect(unique.evidence_id).toBe(current.evidence_id);
|
||||
expect(unique.evidence_links).toEqual(current.evidence_links);
|
||||
expect(uniqueReplay.calls).toEqual([]);
|
||||
// Earlier composed observations did not carry links. Both are authentic,
|
||||
// but the serialized replay binding does not identify which provenance won.
|
||||
const historical = createEvidence(target, current.provider, {
|
||||
operation: current.operation,
|
||||
parameters: current.parameters,
|
||||
result: current.normalized_result,
|
||||
rawResult: current.raw_result,
|
||||
analysisProfile: current.analysis_profile!,
|
||||
confidence: "derived",
|
||||
limitations: current.limitations,
|
||||
});
|
||||
expect(historical.evidence_links).toEqual([]);
|
||||
expect(historical.evidence_id).not.toBe(current.evidence_id);
|
||||
const ambiguous = parseAnalysisSnapshot({
|
||||
...snapshot,
|
||||
evidence_bundle: createEvidenceBundle([
|
||||
...snapshot.evidence_bundle.records,
|
||||
historical,
|
||||
]),
|
||||
});
|
||||
expect(ambiguous.workflow_entries).toEqual(snapshot.workflow_entries);
|
||||
await writeFile(snapshotPath, JSON.stringify(ambiguous));
|
||||
const replay = await composedWorkflowFixture();
|
||||
const delivered = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
replay.dependencies,
|
||||
original.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath },
|
||||
),
|
||||
);
|
||||
expect(delivered.normalized_result).toEqual(current.normalized_result);
|
||||
expect([current.evidence_id, historical.evidence_id]).not.toContain(
|
||||
delivered.evidence_id,
|
||||
);
|
||||
expect(delivered.evidence_links).toEqual(current.evidence_links);
|
||||
expect(
|
||||
delivered.limitations.some((limitation) =>
|
||||
limitation.includes("this call did not re-run the provider"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(replay.calls).toEqual([]);
|
||||
const refreshedReplay = await composedWorkflowFixture();
|
||||
const refreshed = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
refreshedReplay.dependencies,
|
||||
original.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath },
|
||||
),
|
||||
);
|
||||
expect(refreshed.evidence_id).toBe(delivered.evidence_id);
|
||||
expect(refreshed.evidence_links).toEqual(delivered.evidence_links);
|
||||
expect(refreshedReplay.calls).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"links a profiled binary overview only to the atomic record it consumed (cached=%s)",
|
||||
|
||||
Reference in New Issue
Block a user