-
Notifications
You must be signed in to change notification settings - Fork 10
fix(rfc64): preserve Edge periodic sync scope #2039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: codex/rfc64-m1-swm-retirement-correctness
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { PROTOCOL_SYNC, SYSTEM_CONTEXT_GRAPHS } from '@origintrail-official/dkg-core'; | ||
| import { MockChainAdapter } from '@origintrail-official/dkg-chain'; | ||
| import { | ||
| DKGAgent, | ||
| type ContextGraphSubscriptionRecord, | ||
| type ContextGraphSubscriptionStore, | ||
| } from '../src/index.js'; | ||
|
|
||
| const PEER = '12D3KooWSmU3owJvB9sFw8uApDgKrv2VBMecsGGvgAc4Gq6hB57M'; | ||
|
|
||
| async function waitFor(predicate: () => boolean): Promise<void> { | ||
| for (let attempt = 0; attempt < 100; attempt += 1) { | ||
| if (predicate()) return; | ||
| await new Promise((resolve) => setTimeout(resolve, 0)); | ||
| } | ||
| throw new Error('condition did not become true'); | ||
| } | ||
|
|
||
| function cleanDurableSyncResult() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Issue: Avoid duplicating the coverage-evidence agent test harness What's wrong Example Suggested direction For Agents |
||
| return { | ||
| insertedTriples: 0, | ||
| insertedDataTriples: 0, | ||
| insertedMetaTriples: 0, | ||
| fetchedDataTriples: 0, | ||
| fetchedMetaTriples: 0, | ||
| bytesReceived: 0, | ||
| resumedPhases: 0, | ||
| timedOutPhases: 0, | ||
| completedPhases: 10, | ||
| checkpointAdvances: 0, | ||
| emptyResponses: 1, | ||
| metaOnlyResponses: 0, | ||
| verifiedPrivateOnlyResponses: 0, | ||
| dataRejectedMissingMeta: 0, | ||
| rejectedKcs: 0, | ||
| failedPeers: 0, | ||
| failedPhases: 0, | ||
| deniedPhases: 0, | ||
| backoffWorthyFailures: 0, | ||
| deferredBackpressure: 0, | ||
| complete: true, | ||
| }; | ||
| } | ||
|
|
||
| function cleanSharedMemorySyncResult() { | ||
| return { | ||
| insertedTriples: 0, | ||
| insertedDataTriples: 0, | ||
| insertedMetaTriples: 0, | ||
| fetchedDataTriples: 0, | ||
| fetchedMetaTriples: 0, | ||
| bytesReceived: 0, | ||
| resumedPhases: 0, | ||
| timedOutPhases: 0, | ||
| completedPhases: 1, | ||
| checkpointAdvances: 0, | ||
| emptyResponses: 1, | ||
| droppedDataTriples: 0, | ||
| failedPeers: 0, | ||
| failedPhases: 0, | ||
| deniedPhases: 0, | ||
| backoffWorthyFailures: 0, | ||
| deferredBackpressure: 0, | ||
| }; | ||
| } | ||
|
|
||
| async function createRehydratedEdgeEvidenceAgent(contextGraphId: string): Promise<DKGAgent> { | ||
| const persisted = new Map<string, ContextGraphSubscriptionRecord>([[contextGraphId, { | ||
| id: contextGraphId, | ||
| subscribed: true, | ||
| synced: false, | ||
| sharedMemorySynced: false, | ||
| metaSynced: false, | ||
| syncAdmission: 'explicit', | ||
| syncScoped: true, | ||
| }]]); | ||
| const contextGraphSubscriptionStore: ContextGraphSubscriptionStore = { | ||
| loadAll: async () => [...persisted.values()], | ||
| save: async (record) => { persisted.set(record.id, { ...record }); }, | ||
| delete: async (id) => { persisted.delete(id); }, | ||
| }; | ||
| const agent = await DKGAgent.create({ | ||
| name: 'SyncEvidenceEdgePeriodic', | ||
| listenHost: '127.0.0.1', | ||
| nodeRole: 'edge', | ||
| syncContextGraphs: [], | ||
| chainAdapter: new MockChainAdapter(), | ||
| contextGraphSubscriptionStore, | ||
| }); | ||
| (agent as any).started = true; | ||
| (agent as any).networkAdmissionCoordinator.isAcceptedPeer = () => true; | ||
| (agent as any).getPeerProtocols = async () => [PROTOCOL_SYNC]; | ||
| (agent as any).discoverContextGraphsFromStore = async () => 0; | ||
| (agent as any).planSharedMemorySyncContextGraphs = async ( | ||
| _peerId: string, | ||
| contextGraphIds: string[], | ||
| ) => ({ | ||
| publicContextGraphIds: [...contextGraphIds], | ||
| privateRecoverFromCurator: [], | ||
| eligibleContextGraphIds: [...contextGraphIds], | ||
| }); | ||
| (agent as any).refreshMetaSyncedFlags = async () => new Set<string>(); | ||
| (agent as any).hasConfirmedMetaState = async () => true; | ||
| (agent as any).gossip = { | ||
| subscribe: () => undefined, | ||
| unsubscribe: () => undefined, | ||
| onMessage: () => undefined, | ||
| offMessage: () => undefined, | ||
| }; | ||
| (agent.node as any).node = { | ||
| peerId: { toString: () => '12D3KooWLocalEvidencePeer' }, | ||
| }; | ||
| await (agent as any).rehydrateContextGraphSubscriptions(); | ||
| (agent.node as any).node = { | ||
| getPeers: () => [{ toString: () => PEER }], | ||
| getConnections: () => [], | ||
| }; | ||
| (agent as any).getSyncReconcilerProbe = async () => ({ | ||
| protocolsKey: PROTOCOL_SYNC, | ||
| connectionKey: PEER, | ||
| }); | ||
| return agent; | ||
| } | ||
|
|
||
| describe('Edge periodic sync scope evidence', () => { | ||
| it('keeps the normal Edge periodic scope when broad sync-on-connect is enabled', async () => { | ||
| const rehydrated = 'cg-rehydrated-normal-periodic'; | ||
| const runtimeSelected = 'cg-runtime-normal-periodic'; | ||
| const agent = await createRehydratedEdgeEvidenceAgent(rehydrated); | ||
| (agent as any).config.syncOnConnectEnabled = true; | ||
| (agent as any).config.syncSharedMemoryOnConnect = true; | ||
| (agent as any).config.syncContextGraphs.push(runtimeSelected); | ||
| (agent as any).subscribedContextGraphs.set(runtimeSelected, { | ||
| subscribed: true, | ||
| syncMode: 'always-on', | ||
| syncAdmission: 'explicit', | ||
| metaSynced: false, | ||
| }); | ||
| const durableScopes: string[][] = []; | ||
| const sharedMemoryScopes: string[][] = []; | ||
| (agent as any).syncFromPeerDetailed = async ( | ||
| _peerId: string, | ||
| contextGraphIds: string[], | ||
| ) => { | ||
| durableScopes.push([...contextGraphIds]); | ||
| return cleanDurableSyncResult(); | ||
| }; | ||
| (agent as any).syncSharedMemoryFromPeerDetailed = async ( | ||
| _peerId: string, | ||
| contextGraphIds: string[], | ||
| ) => { | ||
| sharedMemoryScopes.push([...contextGraphIds]); | ||
| const summary = cleanSharedMemorySyncResult(); | ||
| return { | ||
| ...summary, | ||
| contextGraphTerminals: contextGraphIds.map((id) => ({ | ||
| contextGraphId: id, | ||
| lane: 'shared_memory' as const, | ||
| disposition: 'settled' as const, | ||
| result: { ...summary }, | ||
| })), | ||
| }; | ||
| }; | ||
|
|
||
| await (agent as any).reconcileSyncFromConnectedPeers(); | ||
| await waitFor(() => (agent as any).lastSuccessfulSyncAt.has(PEER)); | ||
|
|
||
| expect(durableScopes).toEqual([[ | ||
| SYSTEM_CONTEXT_GRAPHS.AGENTS, | ||
| SYSTEM_CONTEXT_GRAPHS.ONTOLOGY, | ||
| rehydrated, | ||
| runtimeSelected, | ||
| ]]); | ||
| expect(sharedMemoryScopes).toEqual([[rehydrated, runtimeSelected]]); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Issue: Make the peer-round scope contract canonical instead of cloning it across layers
What's wrong
This change improves the boolean flag by making bootstrap graphs explicit, but it does so by adding the same new concept to multiple duplicated scope interfaces. That spreads the invariant across layers instead of making the scope model the canonical abstraction, which makes the next scope change harder to reason about and easier to implement inconsistently.
Example
A future change that adds another peer-round scope dimension would need to update
PeerSyncScope,LifecycleSyncScopePlan,SyncOnConnectScopePlan, and the legacy adapter shape separately. Missing one would still compile in some paths because these are structurally typed aliases, leaving the drift to show up as control-flow special casing later.Suggested direction
Define the durable/on-connect scope shape once, then have lifecycle/base plans compose or extend that shape for their extra evidence fields. The legacy adapter can still normalize optional/deprecated input into the canonical plan at the boundary, but the main orchestration should not carry several near-identical interfaces.
Confidence note
This is a structural maintainability finding based on the changed scope model; I did not have an
origin/mainref locally, so the before/after comparison uses the supplied PR diff plus surrounding file reads.For Agents
Look at
packages/agent/src/sync/on-connect/sync-on-connect.ts,packages/agent/src/dkg-agent-base.ts, andpackages/agent/src/dkg-agent-lifecycle.ts. Preserve the current behavior: normal sync includes Agents/Ontology in the first durable request, periodic Edge scoped resume can pass an empty bootstrap list, and the legacy adapter keeps its compatibility default. Extract one canonical peer-round scope type or a small shared base type plus lifecycle-only evidence fields, and centralize the default bootstrap graph constant so adding scope fields is not a multi-file structural edit.