From 8aa40e9a61155cff8b6f3c138c601bf805b20653 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:32:07 +0800 Subject: [PATCH] fix(cache): scope Evidence display refresh to matching snapshot bindings --- docs/cli.md | 6 +- .../binary/AnalysisSnapshotCache.test.ts | 115 +++++++++++++ .../binary/AnalysisSnapshotCache.ts | 57 +++++++ .../binary/BinarySessionRecords.ts | 8 +- .../EvidenceLedger.residualUnknown.test.ts | 6 +- .../investigation/EvidenceLedger.test.ts | 24 ++- .../investigation/EvidenceLedger.ts | 52 +++--- .../InvestigationEvidence.test.ts | 4 +- .../InvestigationRecords.test.ts | 4 +- src/domain/analysisSnapshot.ts | 6 +- .../analysis-sessions/binarySession.test.ts | 158 +++++++++--------- 11 files changed, 327 insertions(+), 113 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 273f8e4b87..84d8c39777 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -309,7 +309,11 @@ an exact result when the target bytes, operation, parameters, provider and settings match. Mutations and cursor-dependent calls are excluded from the cache. Snapshot files are local and use owner-only permissions. -Dispatching a metadata write invalidates the active session's snapshot cache, +Importing an Evidence bundle refreshes retained subject display names and paths. +It preserves cached analysis results, including observations for other targets. +Snapshot bindings that use a replaced record retain its updated display path. + +Dispatching a provider metadata write invalidates the active session's snapshot cache, even if its reply fails: the provider may already have applied the write. Close and reopen the target before importing or saving another snapshot. diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 175ee838a4..386beba199 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -30,6 +30,7 @@ import { SNAPSHOT_CACHE_ENTRY_CEILING, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js"; +import { EvidenceLedger } from "../investigation/EvidenceLedger.js"; import { enhancedOutputSchemas } from "../../contracts/toolOutputSchemaGroups.js"; it("exports independent nested query, workflow and Evidence payloads", () => { @@ -116,6 +117,120 @@ it("exports independent nested query, workflow and Evidence payloads", () => { expect(serializeAnalysisSnapshot(fresh.value)).toBe(expected); }); +it.each(["provider", "workflow"] as const)( + "refreshes only the authenticated %s display dependency and preserves earlier snapshots", + (kind) => { + const cache = new AnalysisSnapshotCache(); + const ledger = new EvidenceLedger(); + const target = ANALYSIS_SNAPSHOT_TARGET; + const profile = ANALYSIS_SNAPSHOT_PROFILE; + const parameters = { address: "0x1000", document: "fixture" }; + const operation = + kind === "provider" ? "procedure_info" : "analyze_function"; + const provider = + kind === "provider" ? ANALYSIS_SNAPSHOT_PROVIDER : REA_WORKFLOW_PROVIDER; + const evidenceProfile = + kind === "provider" ? profile : workflowAnalysisProfile(profile); + const dependencies = [1, 2].map((source) => + createEvidence(target, provider, { + operation: "source", + parameters: { source }, + result: source, + }), + ); + for (const dependency of dependencies) + expect(ledger.record(dependency).ok).toBe(true); + const evidence = createEvidence(target, provider, { + operation, + parameters, + evidenceLinks: dependencies.map(({ evidence_id }) => evidence_id), + result: "original", + analysisProfile: evidenceProfile, + confidence: kind === "provider" ? "observed" : "derived", + }); + const execution = createAnalysisExecution("original", provider, { + subject: target, + analysisProfile: evidenceProfile, + }); + if (kind === "provider") + cache.record({ target, profile, operation, parameters, execution }); + else + cache.recordWorkflow({ + target, + profile, + operation, + parameters, + execution: { ...execution, analysisProfile: evidenceProfile }, + }); + expect(ledger.record(evidence).ok).toBe(true); + const before = cache.export(target, profile, ledger.export()); + if (!before.ok) throw before.error; + const nameOnly = { + ...evidence, + subject: + evidence.subject === null + ? null + : { ...evidence.subject, name: "display-only-name" }, + }; + const nameImport = ledger.import(createEvidenceBundle([nameOnly])); + if (!nameImport.ok) throw nameImport.error; + cache.refreshEvidenceDisplay(nameImport.value.replacements); + expect(cache.entries()).toEqual(before.value.entries); + expect(cache.workflowEntries()).toEqual(before.value.workflow_entries); + // A different observation for this exact query is not the entry's dependency. + const alternative = createEvidence(target, provider, { + operation, + parameters, + result: "alternative", + analysisProfile: evidenceProfile, + confidence: kind === "provider" ? "observed" : "derived", + }); + expect(ledger.record(alternative).ok).toBe(true); + const relocate = (record: typeof evidence) => { + if (record.subject === null) throw new Error("Expected fixture subject"); + return { + ...record, + subject: { + ...record.subject, + local_path: "/relocated/fixture", + name: "renamed", + }, + }; + }; + const foreign = ledger.import( + createEvidenceBundle([relocate(alternative)]), + ); + if (!foreign.ok) throw foreign.error; + cache.refreshEvidenceDisplay(foreign.value.replacements); + expect(cache.entries()).toEqual(before.value.entries); + expect(cache.workflowEntries()).toEqual(before.value.workflow_entries); + // A result may cite several observations; updating their envelopes does not change its payload. + const dependentImport = ledger.import( + createEvidenceBundle(dependencies.map(relocate)), + ); + if (!dependentImport.ok) throw dependentImport.error; + cache.refreshEvidenceDisplay(dependentImport.value.replacements); + expect(cache.entries()).toEqual(before.value.entries); + expect(cache.workflowEntries()).toEqual(before.value.workflow_entries); + const relocated = relocate(evidence); + const imported = ledger.import(createEvidenceBundle([relocated])); + if (!imported.ok) throw imported.error; + cache.refreshEvidenceDisplay(imported.value.replacements); + const after = cache.export(target, profile, ledger.export()); + if (!after.ok) throw after.error; + const entries = + kind === "provider" ? after.value.entries : after.value.workflow_entries; + expect(entries[0]?.execution.subject?.path).toBe("/relocated/fixture"); + expect(entries[0]?.execution.result).toBe("original"); + expect(after.value.evidence_bundle.records).toContainEqual(relocated); + const originalEntries = + kind === "provider" + ? before.value.entries + : before.value.workflow_entries; + expect(originalEntries[0]?.execution.subject?.path).toBe(target.path); + }, +); + it("counts newly retained direct and workflow bindings when staging", () => { const source = new AnalysisSnapshotCache(); const target = ANALYSIS_SNAPSHOT_TARGET; diff --git a/src/application/binary/AnalysisSnapshotCache.ts b/src/application/binary/AnalysisSnapshotCache.ts index bf70bfd2bd..e60981a2c1 100644 --- a/src/application/binary/AnalysisSnapshotCache.ts +++ b/src/application/binary/AnalysisSnapshotCache.ts @@ -13,6 +13,8 @@ import { rebindEvidenceSubjectTargetPath } from "../../domain/evidence.js"; import { err, ok, type Result } from "../../domain/result.js"; import { analysisQueryId, + evidenceMatchesEntry, + evidenceMatchesWorkflowEntry, createAnalysisSnapshotEntry, createAnalysisSnapshotWorkflowEntry, snapshotBinding, @@ -35,6 +37,8 @@ import type { import { OFFICIAL_TOOL_CONTRACTS } from "../../contracts/officialToolContracts.js"; import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; +import type { EvidenceDisplayReplacement } from "../investigation/EvidenceLedger.js"; + const RELATIONSHIP_OPERATIONS: ReadonlySet = new Set([ "procedure_callers", "procedure_callees", @@ -148,6 +152,59 @@ export class AnalysisSnapshotCache { return imported; } + /** Refresh only display bindings backed by the replaced authenticated records. */ + refreshEvidenceDisplay( + replacements: readonly EvidenceDisplayReplacement[], + ): void { + if (this.#target === undefined || this.#binding === undefined) return; + const header = { target: this.#target, binding: this.#binding }; + for (const { previous, current } of replacements) { + if ( + current.subject === null || + previous.subject?.local_path === current.subject.local_path + ) + continue; + const queryId = analysisQueryId( + header.target, + header.binding, + previous.operation, + previous.parameters, + ); + const entry = this.#entries.get(queryId); + if ( + entry !== undefined && + entry.execution.subject !== null && + evidenceMatchesEntry(previous, entry, header) + ) + this.#entries.set(queryId, { + ...entry, + execution: { + ...entry.execution, + subject: { + ...entry.execution.subject, + path: current.subject.local_path, + }, + }, + }); + const workflow = this.#workflowEntries.get(queryId); + if ( + workflow !== undefined && + workflow.execution.subject !== null && + evidenceMatchesWorkflowEntry(previous, workflow, header) + ) + this.#workflowEntries.set(queryId, { + ...workflow, + execution: { + ...workflow.execution, + subject: { + ...workflow.execution.subject, + path: current.subject.local_path, + }, + }, + }); + } + } + /** Build a complete snapshot for an active target. */ export( target: BinaryTarget | undefined, diff --git a/src/application/binary/BinarySessionRecords.ts b/src/application/binary/BinarySessionRecords.ts index f9584a1687..e447d05277 100644 --- a/src/application/binary/BinarySessionRecords.ts +++ b/src/application/binary/BinarySessionRecords.ts @@ -150,8 +150,8 @@ export abstract class BinarySessionRecords { ): Result { const imported = this.#records.mergeEvidenceBundle(bundle); if (!imported.ok) return imported; - if (imported.value.metadataChanged) this.invalidateSnapshot(); - else if (imported.value.changed) this.#emitSnapshotChanged(); + this.#snapshot.refreshEvidenceDisplay(imported.value.replacements); + if (imported.value.changed) this.#emitSnapshotChanged(); return ok(imported.value.recordsAdded); } @@ -238,7 +238,9 @@ export abstract class BinarySessionRecords { : { target: active.target, profile }, (bundle) => { const imported = this.#records.mergeEvidenceBundle(bundle); - return imported.ok ? ok(imported.value.recordsAdded) : imported; + if (!imported.ok) return imported; + this.#snapshot.refreshEvidenceDisplay(imported.value.replacements); + return ok(imported.value.recordsAdded); }, ); if (imported.ok) this.#emitSnapshotChanged(); diff --git a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts index 2885c1906f..229b868044 100644 --- a/src/application/investigation/EvidenceLedger.residualUnknown.test.ts +++ b/src/application/investigation/EvidenceLedger.residualUnknown.test.ts @@ -140,7 +140,7 @@ describe("residual unknown registry reads and identity", () => { recordsAdded: 1, unknownsAdded: 0, changed: true, - metadataChanged: false, + replacements: [], }, }); expect(store.export().unknowns).toEqual(originalUnknowns); @@ -150,7 +150,7 @@ describe("residual unknown registry reads and identity", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); }); @@ -189,7 +189,7 @@ describe("residual unknown dependency histories", () => { recordsAdded: 10_000, unknownsAdded: 10_000, changed: true, - metadataChanged: false, + replacements: [], }, }); expect(store.export()).toEqual(bundle); diff --git a/src/application/investigation/EvidenceLedger.test.ts b/src/application/investigation/EvidenceLedger.test.ts index 9a61df3ad9..f7986e18be 100644 --- a/src/application/investigation/EvidenceLedger.test.ts +++ b/src/application/investigation/EvidenceLedger.test.ts @@ -43,7 +43,7 @@ describe("evidence ledger recording", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); expect(ledger.export().records).toEqual([evidence]); @@ -60,7 +60,13 @@ describe("evidence ledger recording", () => { recordsAdded: 0, unknownsAdded: 0, changed: true, - metadataChanged: true, + replacements: [ + { + kind: "subject-display", + previous: evidence, + current: relocatedBundleRecord, + }, + ], }, }); expect(ledger.get(evidence.evidence_id)).toEqual(relocatedBundleRecord); @@ -73,7 +79,7 @@ describe("evidence ledger recording", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); const conflictingBundle = createEvidenceBundle([ @@ -111,7 +117,7 @@ describe("evidence ledger recording", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); ledger.clear(); @@ -194,7 +200,13 @@ describe("evidence ledger metadata imports", () => { recordsAdded: 0, unknownsAdded: 0, changed: true, - metadataChanged: true, + replacements: [ + { + kind: "subject-display", + previous: mutation, + current: relocatedMutation, + }, + ], }, }); expect(ledger.get(mutation.evidence_id)).toEqual(relocatedMutation); @@ -204,7 +216,7 @@ describe("evidence ledger metadata imports", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); }); diff --git a/src/application/investigation/EvidenceLedger.ts b/src/application/investigation/EvidenceLedger.ts index 338aa8a534..be1061e228 100644 --- a/src/application/investigation/EvidenceLedger.ts +++ b/src/application/investigation/EvidenceLedger.ts @@ -30,12 +30,19 @@ import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection. type EvidenceLedgerFailure = EvidenceIntegrityError; type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>; -/** Describes the state changes made by one atomic bundle import. */ +/** Same authenticated semantic identity with new subject display metadata. */ +export interface EvidenceDisplayReplacement { + readonly kind: "subject-display"; + readonly previous: Evidence; + readonly current: Evidence; +} + +/** Committed changes made by one atomic bundle import. */ export interface EvidenceImportDelta { readonly recordsAdded: number; readonly unknownsAdded: number; readonly changed: boolean; - readonly metadataChanged: boolean; + readonly replacements: readonly EvidenceDisplayReplacement[]; } type ImportResult = Result< @@ -96,8 +103,7 @@ export class EvidenceLedger { // atomic merge without rebuilding and reparsing the entire ledger. if (bundle.unknowns.length === 0) { const additions = new Map(); - const replacements = new Map(); - let metadataChanged = false; + const replacements: EvidenceDisplayReplacement[] = []; for (const evidence of bundle.records) { const existing = this.#records.get(evidence.evidence_id); if (existing !== undefined) { @@ -105,35 +111,39 @@ export class EvidenceLedger { return err( new EvidenceIntegrityError("Conflicting evidence record"), ); - if (canonicalize(existing) !== canonicalize(evidence)) { - replacements.set(evidence.evidence_id, evidence); - metadataChanged = true; + if (!displaySubjectsAgree(existing, evidence)) { + replacements.push({ + kind: "subject-display", + previous: existing, + current: evidence, + }); } continue; } additions.set(evidence.evidence_id, evidence); } - for (const [id, evidence] of replacements) - this.#records.set(id, evidence); + for (const { current } of replacements) + this.#records.set(current.evidence_id, current); for (const [id, evidence] of additions) this.#records.set(id, evidence); return ok({ recordsAdded: additions.size, unknownsAdded: 0, - changed: additions.size > 0 || metadataChanged, - metadataChanged, + changed: additions.size > 0 || replacements.length > 0, + replacements, }); } const pending = new Map(this.#records); - let metadataChanged = false; + const replacements: EvidenceDisplayReplacement[] = []; for (const evidence of bundle.records) { const existing = pending.get(evidence.evidence_id); if (existing !== undefined && !recordsAgree(existing, evidence)) return err(new EvidenceIntegrityError("Conflicting evidence record")); - if ( - existing !== undefined && - canonicalize(existing) !== canonicalize(evidence) - ) - metadataChanged = true; + if (existing !== undefined && !displaySubjectsAgree(existing, evidence)) + replacements.push({ + kind: "subject-display", + previous: existing, + current: evidence, + }); pending.set(evidence.evidence_id, evidence); } const pendingUnknowns = new Map(this.#unknownRevisions); @@ -162,8 +172,8 @@ export class EvidenceLedger { return ok({ recordsAdded: added, unknownsAdded, - changed: added > 0 || unknownsAdded > 0 || metadataChanged, - metadataChanged, + changed: added > 0 || unknownsAdded > 0 || replacements.length > 0, + replacements, }); } @@ -518,3 +528,7 @@ const withoutLocalPath = (evidence: Evidence): Evidence => ({ local_path: "", }, }); + +const displaySubjectsAgree = (left: Evidence, right: Evidence): boolean => + left.subject?.name === right.subject?.name && + left.subject?.local_path === right.subject?.local_path; diff --git a/src/application/investigation/InvestigationEvidence.test.ts b/src/application/investigation/InvestigationEvidence.test.ts index 571f4bfe5f..a33120a550 100644 --- a/src/application/investigation/InvestigationEvidence.test.ts +++ b/src/application/investigation/InvestigationEvidence.test.ts @@ -43,7 +43,9 @@ it.each(["immutable", "detached"] as const)( createEvidenceBundle([replacement]), ); if (!merged.ok) throw merged.error; - expect(merged.value.metadataChanged).toBe(true); + expect(merged.value.replacements).toEqual([ + { kind: "subject-display", previous: first, current: replacement }, + ]); expect(lookup(first.evidence_id)?.subject?.local_path).toBe("/moved"); expect( investigationEvidenceBundle(reader).records[0]?.subject?.local_path, diff --git a/src/application/investigation/InvestigationRecords.test.ts b/src/application/investigation/InvestigationRecords.test.ts index 1f28f4c6d6..9d3d873248 100644 --- a/src/application/investigation/InvestigationRecords.test.ts +++ b/src/application/investigation/InvestigationRecords.test.ts @@ -158,7 +158,7 @@ describe("investigation record ownership", () => { recordsAdded: 0, unknownsAdded: 1, changed: true, - metadataChanged: false, + replacements: [], }, }); expect(records.mergeEvidenceBundle(bundle)).toEqual({ @@ -167,7 +167,7 @@ describe("investigation record ownership", () => { recordsAdded: 0, unknownsAdded: 0, changed: false, - metadataChanged: false, + replacements: [], }, }); records.clear(); diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 491c784aba..d728182df2 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -529,7 +529,8 @@ export const createAnalysisSnapshotWorkflowEntry = (input: { }; }; -const evidenceMatchesEntry = ( +/** Match an authenticated Evidence record to one exact provider cache binding. */ +export const evidenceMatchesEntry = ( evidence: Evidence, entry: AnalysisSnapshotEntry, snapshot: Pick, @@ -871,7 +872,8 @@ const isWorkflowEvidence = ( evidence.provider.name === entry.execution.provider.name && evidence.provider.version === entry.execution.provider.version; -const evidenceMatchesWorkflowEntry = ( +/** Match an authenticated Evidence record to one exact composed cache binding. */ +export const evidenceMatchesWorkflowEntry = ( evidence: Evidence, entry: AnalysisSnapshotWorkflowEntry, snapshot: Pick, diff --git a/tests/composition/analysis-sessions/binarySession.test.ts b/tests/composition/analysis-sessions/binarySession.test.ts index 20a537f3a6..0e4ab9d80e 100644 --- a/tests/composition/analysis-sessions/binarySession.test.ts +++ b/tests/composition/analysis-sessions/binarySession.test.ts @@ -576,82 +576,88 @@ describe("evidence metadata imports and snapshot cache", () => { await session.close(); }); - it("keeps changed evidence metadata out of snapshots until the session is recreated", async () => { - const [target] = await createBinarySessionTargets(); - const calls: string[] = []; - const session = createTestBinarySession(createCacheProvider(calls)); - expect((await session.open(target)).ok).toBe(true); - expect( - ( - await session.execute("address_name", { - address: "0x1000", - document: "first", - }) - ).ok, - ).toBe(true); - const baseline = session.exportAnalysisSnapshot(); - if (!baseline.ok) throw baseline.error; - - const evidence = session - .exportEvidenceBundle() - .records.find(({ operation }) => operation === "address_name"); - if (evidence === undefined || evidence.subject === null) - throw new Error("Expected artifact-bound Evidence"); - const callsAfterInitialRead = [...calls]; - - const relocated = { - ...evidence, - subject: { - ...evidence.subject, - local_path: `${evidence.subject.local_path}.moved`, - name: `${evidence.subject.name}.moved`, - }, - }; - expect( - session.importEvidenceBundle(createEvidenceBundle([relocated])), - ).toEqual({ ok: true, value: 0 }); - expect(session.exportEvidenceBundle().records).toContainEqual(relocated); - expect( - ( - await session.execute("address_name", { - address: "0x1000", - document: "first", - }) - ).ok, - ).toBe(true); - expect(calls).toHaveLength(callsAfterInitialRead.length + 1); - expect(session.exportAnalysisSnapshot()).toMatchObject({ - ok: false, - error: { - _tag: "EvidenceIntegrityError", - }, - }); - expect((await session.open(target)).ok).toBe(true); - expect(session.exportAnalysisSnapshot().ok).toBe(false); - expect(session.importAnalysisSnapshot(baseline.value)).toMatchObject({ - ok: false, - error: { - _tag: "EvidenceIntegrityError", - }, - }); - expect(session.exportEvidenceBundle().records).toContainEqual(relocated); - const callsAfterInvalidation = calls.length; - expect( - ( - await session.execute("address_name", { - address: "0x1000", - document: "first", - }) - ).ok, - ).toBe(true); - expect(calls).toHaveLength(callsAfterInvalidation + 1); - await session.close(); - expect((await session.open(target, { snapshot: baseline.value })).ok).toBe( - true, - ); - expect(session.exportAnalysisSnapshot().ok).toBe(true); - await session.close(); - }); + it.each(["active", "unrelated"] as const)( + "refreshes %s Evidence display metadata without invalidating provider observations", + async (scope) => { + const [target] = await createBinarySessionTargets(); + const calls: string[] = []; + const session = createTestBinarySession(createCacheProvider(calls)); + try { + expect((await session.open(target)).ok).toBe(true); + const parameters = { address: "0x1000", document: "first" }; + const initial = await session.execute("address_name", parameters); + if (!initial.ok) throw initial.error; + const baseline = session.exportAnalysisSnapshot(); + if (!baseline.ok) throw baseline.error; + const activeEvidence = baseline.value.evidence_bundle.records.find( + ({ operation }) => operation === "address_name", + ); + if (activeEvidence === undefined || activeEvidence.subject === null) + throw new Error("Expected artifact-bound Evidence"); + const evidence = + scope === "active" + ? activeEvidence + : createEvidence( + { path: "/unrelated", sha256: "f".repeat(64), format: "elf" }, + { id: "fixture", name: "Fixture", version: "1" }, + { operation: "health", parameters: {}, result: true }, + ); + expect(session.recordEvidence(evidence).ok).toBe(true); + if (evidence.subject === null) throw new Error("Expected a subject"); + const relocated = { + ...evidence, + subject: { + ...evidence.subject, + local_path: `${evidence.subject.local_path}.moved`, + name: `${evidence.subject.name}.moved`, + }, + }; + const changed = vi.fn(); + const unsubscribe = session.onAnalysisSnapshotChanged(changed); + expect( + session.importEvidenceBundle(createEvidenceBundle([relocated])), + ).toEqual({ ok: true, value: 0 }); + expect(changed).toHaveBeenCalledTimes(1); + unsubscribe(); + expect(session.evidenceById(evidence.evidence_id)).toEqual(relocated); + const refreshed = session.exportAnalysisSnapshot(); + if (!refreshed.ok) throw refreshed.error; + if (scope === "active") { + expect(refreshed.value.entries[0]?.execution.subject?.path).toBe( + relocated.subject.local_path, + ); + expect(refreshed.value.evidence_bundle.records).toContainEqual( + relocated, + ); + } else { + expect(refreshed.value.entries).toEqual(baseline.value.entries); + expect(refreshed.value.evidence_bundle.records).not.toContainEqual( + relocated, + ); + } + const callsAfterInitialRead = [...calls]; + const replay = await session.execute("address_name", parameters); + if (!replay.ok) throw replay.error; + expect(replay.value.result).toEqual(initial.value.result); + expect(replay.value.subject?.path).toBe(target); + expect(calls).toEqual(callsAfterInitialRead); + // Import follows the same display-refresh path without poisoning the session. + expect( + session.importAnalysisSnapshot({ + ...baseline.value, + entries: [], + workflow_entries: [], + }).ok, + ).toBe(true); + expect(session.exportAnalysisSnapshot()).toMatchObject({ + ok: true, + value: { entries: baseline.value.entries }, + }); + } finally { + expect(await session.close()).toEqual({ ok: true, value: null }); + } + }, + ); }); describe("snapshot invalidation at mutation dispatch", () => {