diff --git a/packages/agent/src/dkg-agent-lifecycle.ts b/packages/agent/src/dkg-agent-lifecycle.ts index 138187f067..81fb212246 100644 --- a/packages/agent/src/dkg-agent-lifecycle.ts +++ b/packages/agent/src/dkg-agent-lifecycle.ts @@ -5723,36 +5723,27 @@ export class LifecycleSyncMethods extends DKGAgentBase { const graphManager = new GraphManager(this.store); await graphManager.ensureContextGraph(contextGraphId); }, - // Everything needed to materialize verified public SWM snapshots, - // as ONE dependency (a loose optional trio allowed a silent - // half-configured mode). Graph-scoped (contentScopeVersion 2) KAs - // carry no dkg:rootEntity, so the aggregate data phase returns 0 - // data quads for them by design — their content arrives as - // immutable snapshots, and without this the catch-up lane cached - // every verified snapshot and never wrote one to the store. - // Thin wiring only: the materialization policy (content-digest - // guard, MAX head read + duplicate repair, atomic replace, head - // metadata swap) lives in `swm-snapshot-materializer.ts`. What - // the agent contributes here is its own resources — the store, - // the SAME lock map injected into SharedMemoryHandler (sharing - // the map + key helper is what closes the check-then-replace - // race with gossip), and list-cache invalidation. + // Graph-scoped (contentScopeVersion 2) KAs carry no + // dkg:rootEntity, so their content arrives as immutable + // snapshots rather than aggregate data quads. Materialization + // and post-commit retirement remain explicit dependencies so the + // sync coordinator owns their required ordering. snapshotMaterializer: createSharedMemorySnapshotMaterializer({ store: this.store, writeLocks: this.writeLocks, invalidateListContextGraphsCache: () => this.invalidateListContextGraphsCache(), - settleGraphScopedSnapshot: async (contextGraphId, descriptor) => { - await this.getOrCreateFinalizationHandler() - .retireSyncedGraphScopedSwmIfFinalized({ - contextGraphId, - ual: descriptor.kaUal, - assertionVersion: descriptor.assertionVersion, - ...(descriptor.subGraphName - ? { subGraphName: descriptor.subGraphName } - : {}), - }, ctx); - }, }), + settleGraphScopedSnapshot: async (contextGraphId, descriptor) => { + await this.getOrCreateFinalizationHandler() + .retireSyncedGraphScopedSwmIfFinalized({ + contextGraphId, + ual: descriptor.kaUal, + assertionVersion: descriptor.assertionVersion, + ...(descriptor.subGraphName + ? { subGraphName: descriptor.subGraphName } + : {}), + }, ctx); + }, storeInsert: async (quads) => { // Oversize guard (OT-RFC-56): drop+tombstone protocol-violating // literals BEFORE insert so the SWM page cursor advances instead diff --git a/packages/agent/src/finalization-handler.ts b/packages/agent/src/finalization-handler.ts index e98763f198..93365690a0 100644 --- a/packages/agent/src/finalization-handler.ts +++ b/packages/agent/src/finalization-handler.ts @@ -1202,6 +1202,7 @@ export class FinalizationHandler { blockNumber: verifiedBlockNumber, txIndex: verifiedTxIndex, }; + let outcome: 'already-confirmed' | 'applied' | 'preserved-metadata' | 'stale' | undefined; if (vmVerification.status === 'verified') { const metadataState = await this.graphScopedMetadataState({ contextGraphId, @@ -1217,46 +1218,50 @@ export class FinalizationHandler { subGraphName, }); if (metadataState === 'matching') { - await this.markMatchingGraphScopedSwmFinalized({ - contextGraphId, - scope, - merkleRoot: msg.kcMerkleRoot, - subGraphName, - ctx, - }); - this.markProcessed(dedupeKey); - this.log.info(ctx, `Finalization: graph-scoped KA ${scope.ual} is already confirmed`); - return 'already-confirmed'; + outcome = 'already-confirmed'; } } + if (!outcome) { + outcome = await this.applyVerifiedGraphScopedFinalization({ + contextGraphId, + scope, + verifiedQuads: layerVerification.quads, + head, + privateMerkleRoot, + computedMerkleRoot: layerVerification.merkleRoot, + publisherAddress: msg.publisherAddress, + txHash: msg.txHash, + blockNumber: verifiedBlockNumber, + batchId, + authorAddress: verifiedAuthorAddress, + materializedVersion, + accessPolicy, + allowedPeers, + subGraphName, + source: 'finalization', + contentAlreadyMaterialized: vmVerification.status === 'verified', + ctx, + }); + } + if (outcome === 'stale') { + this.markProcessed(dedupeKey); + this.log.info(ctx, `Finalization: newer graph-scoped assertion already materialized for ${scope.ual}`); + return 'already-confirmed'; + } - const outcome = await this.applyVerifiedGraphScopedFinalization({ + await this.markMatchingGraphScopedSwmFinalized({ contextGraphId, scope, - verifiedQuads: layerVerification.quads, - head, - privateMerkleRoot, - computedMerkleRoot: layerVerification.merkleRoot, - publisherAddress: msg.publisherAddress, - txHash: msg.txHash, - blockNumber: verifiedBlockNumber, - batchId, - authorAddress: verifiedAuthorAddress, - materializedVersion, - accessPolicy, - allowedPeers, + merkleRoot: msg.kcMerkleRoot, subGraphName, - source: 'finalization', - contentAlreadyMaterialized: vmVerification.status === 'verified', ctx, }); - if (outcome === 'stale') { - this.markProcessed(dedupeKey); - this.log.info(ctx, `Finalization: newer graph-scoped assertion already materialized for ${scope.ual}`); - return 'already-confirmed'; - } this.markProcessed(dedupeKey); + if (outcome === 'already-confirmed') { + this.log.info(ctx, `Finalization: graph-scoped KA ${scope.ual} is already confirmed`); + return 'already-confirmed'; + } this.log.info( ctx, `Finalization: promoted graph-scoped KA ${scope.ual} (${publicTripleCount} public, ${privateTripleCount} private)`, @@ -1660,19 +1665,10 @@ export class FinalizationHandler { subGraphName, }); if (vmVerification.status === 'verified') { - // A cold join can receive the durable VM snapshot and its still-present - // SWM recovery snapshot from different catch-up planes without ever - // seeing the live finalization envelope. Exact VM content plus the - // chain-resolved root is sufficient to retire only the matching current - // SWM head from the user-facing view. This does not synthesize confirmed - // transaction metadata: provenance repair remains pending below. - await this.markMatchingGraphScopedSwmFinalized({ - contextGraphId, - scope, - merkleRoot, - subGraphName, - ctx, - }); + let acceptedOutcome: + | 'stale-target' + | 'already-confirmed' + | 'verified-vm-metadata-pending'; const access = resolveGraphScopedAccessEnvelope( head, trustedAssertionEvidence?.accessPolicy, @@ -1697,9 +1693,8 @@ export class FinalizationHandler { materializedVersion, }); this.log.info(ctx, `Chain-reconcile: ${ual} already has exact VM content and metadata`); - return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed'; - } - if (!trustedAssertionEvidence && access.accessPolicy !== 'ownerOnly') { + acceptedOutcome = preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed'; + } else if (!trustedAssertionEvidence && access.accessPolicy !== 'ownerOnly') { const failClosedMetadataState = await this.graphScopedMetadataState({ contextGraphId, scope, @@ -1721,51 +1716,77 @@ export class FinalizationHandler { ctx, `Chain-reconcile: ${ual} retains fail-closed access without assertion evidence`, ); - return 'already-confirmed'; + acceptedOutcome = 'already-confirmed'; + } else { + // No exact metadata is accepted. Without assertion-bound provenance + // the receiver may retire only the matching SWM recovery copy; it + // must leave confirmed metadata explicitly pending. + this.log.info( + ctx, + `Chain-reconcile: exact VM metadata for ${ual} cannot be repaired without ` + + 'transaction provenance; deferring', + ); + acceptedOutcome = 'verified-vm-metadata-pending'; } - } - if (!trustedAssertionEvidence) { + } else if (!trustedAssertionEvidence) { + // A cold join can receive the durable VM snapshot and its still-present + // SWM recovery snapshot from different catch-up planes without ever + // seeing the live finalization envelope. Exact VM content plus the + // chain-resolved root retires only the matching current SWM head while + // transaction provenance remains explicitly pending. this.log.info( ctx, `Chain-reconcile: exact VM metadata for ${ual} cannot be repaired without ` + 'transaction provenance; deferring', ); - return 'verified-vm-metadata-pending'; + acceptedOutcome = 'verified-vm-metadata-pending'; + } else { + // A confirmed publish may have committed the exact VM graph before its + // graph-scoped metadata survived a crash. Reapply only the metadata + // tail. A failed write throws before the single retirement guard below, + // so the SWM recovery copy stays visible. + const outcome = await this.applyVerifiedGraphScopedFinalization({ + contextGraphId, + scope, + verifiedQuads: vmVerification.quads, + head, + privateMerkleRoot, + computedMerkleRoot: vmVerification.merkleRoot, + publisherAddress: evidencePublisherAddress, + txHash: trustedAssertionEvidence.transactionHash, + blockNumber: evidenceBlockNumber, + batchId: kaId, + authorAddress: evidenceAuthorAddress, + materializedVersion, + accessPolicy: trustedAssertionEvidence.accessPolicy, + allowedPeers: trustedAssertionEvidence.allowedPeers, + subGraphName, + source: 'chain-reconcile', + contentAlreadyMaterialized: true, + ctx, + }); + if (outcome === 'stale') return 'stale-target'; + if (outcome === 'preserved-metadata') { + this.log.info( + ctx, + `Chain-reconcile: retained confirmed metadata for an older same-root assertion ${ual}`, + ); + } else { + this.log.info(ctx, `Chain-reconcile: exact VM graph already matches ${ual}; repaired metadata`); + } + acceptedOutcome = preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed'; } - // A confirmed publish may have committed the exact VM graph before its - // graph-scoped metadata survived a crash. Reapply only the metadata tail: - // SWM writers use a different lock, so this recovery path must not delete - // a potentially newer staged assertion. - const outcome = await this.applyVerifiedGraphScopedFinalization({ + + // One policy guard for every accepted exact-VM branch. Any metadata or + // ordering write above must succeed before the SWM copy can be hidden. + await this.markMatchingGraphScopedSwmFinalized({ contextGraphId, scope, - verifiedQuads: vmVerification.quads, - head, - privateMerkleRoot, - computedMerkleRoot: vmVerification.merkleRoot, - publisherAddress: evidencePublisherAddress, - txHash: trustedAssertionEvidence.transactionHash, - blockNumber: evidenceBlockNumber, - batchId: kaId, - authorAddress: evidenceAuthorAddress, - materializedVersion, - accessPolicy: trustedAssertionEvidence?.accessPolicy, - allowedPeers: trustedAssertionEvidence?.allowedPeers, + merkleRoot, subGraphName, - source: 'chain-reconcile', - contentAlreadyMaterialized: true, ctx, }); - if (outcome === 'stale') return 'stale-target'; - if (outcome === 'preserved-metadata') { - this.log.info( - ctx, - `Chain-reconcile: retained confirmed metadata for an older same-root assertion ${ual}`, - ); - return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed'; - } - this.log.info(ctx, `Chain-reconcile: exact VM graph already matches ${ual}; repaired metadata`); - return preserveNewerWorkspaceLifecycle ? 'stale-target' : 'already-confirmed'; + return acceptedOutcome; } const swmVerification = await this.verifyExactGraphScopedLayer({ @@ -1836,6 +1857,13 @@ export class FinalizationHandler { ctx, }); if (outcome === 'stale') return 'stale-target'; + await this.markMatchingGraphScopedSwmFinalized({ + contextGraphId, + scope, + merkleRoot, + subGraphName, + ctx, + }); if (outcome === 'preserved-metadata') { this.log.info( ctx, @@ -2035,15 +2063,7 @@ export class FinalizationHandler { } return 'applied' as const; }); - if (outcome !== 'applied') return outcome; - - await this.markMatchingGraphScopedSwmFinalized({ - contextGraphId, - scope, - merkleRoot: computedMerkleRoot, - subGraphName, - ctx, - }); + if (outcome === 'stale' || outcome === 'preserved-metadata') return outcome; this.eventBus?.emit(DKGEvent.MEMORY_GRAPH_CHANGED, { contextGraphId, diff --git a/packages/agent/src/sync/requester/shared-memory-sync.ts b/packages/agent/src/sync/requester/shared-memory-sync.ts index c0068de521..44e880214b 100644 --- a/packages/agent/src/sync/requester/shared-memory-sync.ts +++ b/packages/agent/src/sync/requester/shared-memory-sync.ts @@ -68,19 +68,16 @@ interface SharedMemorySyncContext { }>; ensureContextGraph: (contextGraphId: string) => Promise; storeInsert: (quads: Quad[]) => Promise; + /** Store adapter for verified public SWM snapshots. */ + snapshotMaterializer?: SharedMemorySnapshotMaterializer; /** - * Everything needed to MATERIALIZE verified public SWM snapshots into the - * triple store, as ONE cohesive dependency — the contract (and the - * production implementation) live in `swm-snapshot-materializer.ts`. - * - * Why it exists at all: contentScopeVersion-2 KAs carry no dkg:rootEntity, - * so the aggregate data phase legitimately returns 0 data quads for them — - * their content travels as immutable snapshots. The catch-up lane fetched - * and VERIFIED those snapshots and then never wrote them, so a node that - * missed the live gossip stayed empty forever ("0 data + N meta triples"). - * Absent entirely => materialization is skipped (never half-applied). + * Post-commit policy for a materialized snapshot. It runs only after the + * verified metadata insert succeeds and outside the per-KA write lock. */ - snapshotMaterializer?: SharedMemorySnapshotMaterializer; + settleGraphScopedSnapshot?: ( + contextGraphId: string, + descriptor: GraphScopedSwmRecoveryDescriptor, + ) => Promise; publicSnapshotStore?: WorkspacePublicSnapshotStore; getRegisteredSubGraphNames?: (contextGraphId: string) => Promise; getExcludedSubGraphNames?: (contextGraphId: string) => Promise; @@ -119,6 +116,7 @@ export async function runSharedMemorySync(context: SharedMemorySyncContext): Pro ensureContextGraph, storeInsert, snapshotMaterializer, + settleGraphScopedSnapshot, publicSnapshotStore, getRegisteredSubGraphNames, getExcludedSubGraphNames, @@ -468,15 +466,15 @@ export async function runSharedMemorySync(context: SharedMemorySyncContext): Pro summary.insertedTriples += processed.verifiedMeta.length; summary.insertedMetaTriples += processed.verifiedMeta.length; } - // Deliberately outside the per-KA snapshot lock above: the finalizer - // acquires the same lock before stamping the local retirement marker. - // Running settlement inside the materializer critical section would - // deadlock. The handoff remains part of the ONE required materializer - // contract, so production cannot wire materialization without it. - await snapshotMaterializer?.settleCommittedSnapshots( - pid, - [...settledDescriptors.values()], - ); + // Deliberately after the verified metadata insert and outside the per-KA + // snapshot lock above: the finalizer acquires that same lock before + // stamping the local retirement marker. A failed insert therefore keeps + // the recovery snapshot visible and cannot deadlock settlement. + if (settleGraphScopedSnapshot) { + for (const descriptor of settledDescriptors.values()) { + await settleGraphScopedSnapshot(pid, descriptor); + } + } recordPhaseOutcome(wsMetaResult); recordPhaseOutcome(wsDataResult); if ((wsMetaResult.timedOut || wsDataResult.timedOut) && shouldStopAfterBackoffWorthyFailure(pid, 'phase timeout')) { diff --git a/packages/agent/src/sync/requester/swm-snapshot-materializer.ts b/packages/agent/src/sync/requester/swm-snapshot-materializer.ts index 6b75bb6793..2178bc6a55 100644 --- a/packages/agent/src/sync/requester/swm-snapshot-materializer.ts +++ b/packages/agent/src/sync/requester/swm-snapshot-materializer.ts @@ -4,10 +4,9 @@ * This module OWNS the persistence policy for turning a verified graph-scoped * snapshot into durable store state: what "already materialized" means, how * the stored head version is read, how stale head metadata is replaced, and - * which lock serializes it all against live gossip. `runSharedMemorySync` - * consumes it as one cohesive dependency (see `SharedMemorySnapshotMaterializer`); - * `dkg-agent-lifecycle` is reduced to wiring agent-owned resources into - * `createSharedMemorySnapshotMaterializer`. + * which lock serializes it all against live gossip. The higher-level snapshot + * committer composes this store adapter with post-commit settlement; + * `dkg-agent-lifecycle` only wires agent-owned resources into both. * * Every SPARQL read/write here is scoped to an exact per-KA IRI (the head * subject, the operation subject, or the KA's own assertion graph), so each @@ -45,8 +44,8 @@ export interface StoredWorkspaceHeadState { } /** - * Everything `runSharedMemorySync` needs to MATERIALIZE verified public SWM - * snapshots into the triple store, as ONE cohesive dependency. + * Everything the snapshot committer needs to MATERIALIZE verified public SWM + * snapshots into the triple store, as one cohesive store dependency. * * Why one object: these capabilities are only meaningful together. An earlier * revision exposed them as independent optionals, which allowed a silent @@ -103,19 +102,6 @@ export interface SharedMemorySnapshotMaterializer { contextGraphId: string, descriptor: GraphScopedSwmRecoveryDescriptor, ): Promise; - /** - * Complete the graph-scoped snapshot transaction after the caller has - * inserted the verified head metadata. Keeping this handoff on the same - * required abstraction prevents a caller from materializing snapshots while - * silently omitting post-commit finalization settlement. - * - * This MUST be invoked outside `withKaWriteLock`: the finalization owner - * takes that same per-KA lock while revalidating the current SWM head. - */ - settleCommittedSnapshots( - contextGraphId: string, - descriptors: readonly GraphScopedSwmRecoveryDescriptor[], - ): Promise; } /** @@ -130,10 +116,6 @@ export function createSharedMemorySnapshotMaterializer(deps: { */ writeLocks: Map>; invalidateListContextGraphsCache: () => void; - settleGraphScopedSnapshot: ( - contextGraphId: string, - descriptor: GraphScopedSwmRecoveryDescriptor, - ) => Promise; }): SharedMemorySnapshotMaterializer { return { withKaWriteLock: (contextGraphId, subGraphName, kaUal, fn) => @@ -248,12 +230,6 @@ export function createSharedMemorySnapshotMaterializer(deps: { ); } }, - - settleCommittedSnapshots: async (contextGraphId, descriptors) => { - for (const descriptor of descriptors) { - await deps.settleGraphScopedSnapshot(contextGraphId, descriptor); - } - }, }; } diff --git a/packages/agent/test/ka-graph-finalization-handler.test.ts b/packages/agent/test/ka-graph-finalization-handler.test.ts index 7ee1c5c899..c528b6809d 100644 --- a/packages/agent/test/ka-graph-finalization-handler.test.ts +++ b/packages/agent/test/ka-graph-finalization-handler.test.ts @@ -2288,6 +2288,121 @@ describe('graph-scoped finalization handler', () => { )).resolves.toMatchObject({ type: 'boolean', value: false }); }); + it('keeps exact SWM visible when trusted VM metadata repair fails', async () => { + const { message, swmGraph, vmGraph } = await stageGraph(); + const swmResult = await store.query( + `CONSTRUCT { ?s ?p ?o } WHERE { GRAPH <${swmGraph}> { ?s ?p ?o } }`, + ); + if (swmResult.type !== 'quads') throw new Error('expected staged SWM quads'); + await store.dropGraph(vmGraph); + await store.insert(swmResult.quads.map((quad) => ({ ...quad, graph: vmGraph }))); + await store.deleteByPattern({ + graph: `did:dkg:context-graph:${CG}/_meta`, + subject: UAL, + }); + + const repairHandler = new FinalizationHandler( + store, + legacyFinalizationChain(), + { writeLocks: new Map() }, + ); + const internals = repairHandler as unknown as { + verifyChainCgBinding: (kaId: bigint, cgId: string) => Promise; + }; + internals.verifyChainCgBinding = async () => true; + const replaceGraphAndSubject = store.replaceGraphAndSubject?.bind(store); + if (!replaceGraphAndSubject) throw new Error('Oxigraph replaceGraphAndSubject unavailable'); + store.replaceGraphAndSubject = async () => { + throw new StoreSchedulerBusyError('queue_wait_timeout', 'normal', 'sparql-http.update'); + }; + + try { + await expect(repairHandler.handleChainReconciledKC({ + contextGraphId: CG, + onChainCgId: '42', + ual: UAL, + merkleRoot: message.kcMerkleRoot, + publisherAddress: PUBLISHER, + kaId: PACKED_KA_ID, + versionBlock: 123, + authorAddress: AUTHOR, + trustedAssertionEvidence: trustedRecoveryEvidence(message), + }, createOperationContext('system'))).rejects.toBeInstanceOf(StoreSchedulerBusyError); + } finally { + store.replaceGraphAndSubject = replaceGraphAndSubject; + } + + await expect(store.query( + `ASK { GRAPH <${LOCAL_TRUSTED_KA_CONTROLS_GRAPH}> { + <${swmGraph}> <${DKG_SWM_FINALIZED_PREDICATE}> ?version . + } }`, + )).resolves.toMatchObject({ type: 'boolean', value: false }); + const visible = await new DKGQueryEngine(store).query( + 'SELECT ?value WHERE { ?s ?value }', + { contextGraphId: CG, view: 'shared-working-memory' }, + ); + expect(visible.bindings).toHaveLength(2); + }); + + it('keeps exact SWM visible when matching VM version advancement fails', async () => { + const { message, swmGraph } = await stageGraph(); + await handler.handleFinalizationMessage( + encodeFinalizationMessage(message), + CG, + '12D3KooWPublisher', + ); + // Simulate a cold receiver materializing the retained recovery copy after + // the exact VM and its confirmed metadata have already landed. + await store.deleteByPattern({ + graph: LOCAL_TRUSTED_KA_CONTROLS_GRAPH, + subject: swmGraph, + }); + + const recoveryHandler = new FinalizationHandler( + store, + legacyFinalizationChain(), + { writeLocks: new Map() }, + ); + const internals = recoveryHandler as unknown as { + verifyChainCgBinding: (kaId: bigint, cgId: string) => Promise; + }; + internals.verifyChainCgBinding = async () => true; + + const insert = store.insert.bind(store); + store.insert = async (quads, options) => { + if (quads.some((quad) => quad.predicate === 'http://dkg.io/ontology/materializedVersion')) { + throw new StoreSchedulerBusyError('queue_wait_timeout', 'normal', 'sparql-http.insert'); + } + return insert(quads, options); + }; + + try { + await expect(recoveryHandler.handleChainReconciledKC({ + contextGraphId: CG, + onChainCgId: '42', + ual: UAL, + merkleRoot: message.kcMerkleRoot, + publisherAddress: PUBLISHER, + kaId: PACKED_KA_ID, + versionBlock: 124, + authorAddress: AUTHOR, + }, createOperationContext('system'))).rejects.toBeInstanceOf(StoreSchedulerBusyError); + } finally { + store.insert = insert; + } + + await expect(store.query( + `ASK { GRAPH <${LOCAL_TRUSTED_KA_CONTROLS_GRAPH}> { + <${swmGraph}> <${DKG_SWM_FINALIZED_PREDICATE}> ?version . + } }`, + )).resolves.toMatchObject({ type: 'boolean', value: false }); + const visible = await new DKGQueryEngine(store).query( + 'SELECT ?value WHERE { ?s ?value }', + { contextGraphId: CG, view: 'shared-working-memory' }, + ); + expect(visible.bindings).toHaveLength(2); + }); + it('retires a synced SWM copy with separate batch provenance after exact chain revalidation', async () => { const staged = await stageGraph(); // `batchId` is adapter provenance and need not equal the UAL-derived packed diff --git a/packages/agent/test/swm-public-snapshot-materialization.test.ts b/packages/agent/test/swm-public-snapshot-materialization.test.ts index 75821a02af..5daaab3eeb 100644 --- a/packages/agent/test/swm-public-snapshot-materialization.test.ts +++ b/packages/agent/test/swm-public-snapshot-materialization.test.ts @@ -202,14 +202,12 @@ function harness(overrides: HarnessOverrides = {}) { events.push('head-swapped'); headSwaps.push({ contextGraphId, headSubject: descriptor.headSubject }); }, - settleCommittedSnapshots: async (contextGraphId, descriptors) => { - for (const descriptor of descriptors) { - expect(contextGraphId).toBe(CG); - expect(descriptor.kaUal).toBe(UAL); - events.push('snapshot-settled'); - await overrides.onGraphScopedSnapshotSettled?.(); - } - }, + }, + settleGraphScopedSnapshot: async (contextGraphId, descriptor) => { + expect(contextGraphId).toBe(CG); + expect(descriptor.kaUal).toBe(UAL); + events.push('snapshot-settled'); + await overrides.onGraphScopedSnapshotSettled?.(); }, publicSnapshotStore: snapshotStore, deleteCheckpoint: () => {}, diff --git a/packages/agent/test/swm-snapshot-materializer.test.ts b/packages/agent/test/swm-snapshot-materializer.test.ts index b0d4d5b948..975adf4033 100644 --- a/packages/agent/test/swm-snapshot-materializer.test.ts +++ b/packages/agent/test/swm-snapshot-materializer.test.ts @@ -116,7 +116,6 @@ function materializerFor(store: TripleStore) { store, writeLocks: new Map>(), invalidateListContextGraphsCache: () => { invalidations += 1; }, - settleGraphScopedSnapshot: async () => {}, }); return { materializer, invalidations: () => invalidations }; } @@ -292,6 +291,7 @@ describe('createSharedMemorySnapshotMaterializer against a real OxigraphStore', return materializer.replaceGraph(graphUri, quads); }, }, + settleGraphScopedSnapshot: async () => {}, publicSnapshotStore: snapshotStore, deleteCheckpoint: () => {}, setCheckpoint: () => {},