Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,6 @@ describe('RFC-64 legacy SWM boundary (live Blazegraph)', () => {
);
expect(HISTORICAL_OPERATION_ROWS).toBeGreaterThan(100_000);
expect(firstHeadReadCount).toBe(1);
const captureQueries = querySpy.mock.calls.filter(([, options]) => (
options?.source === 'agent.rfc64.legacySwmBoundary.readHeads'
)).map(([sparql]) => sparql);
expect(captureQueries).toHaveLength(1);
expect(captureQueries[0]).toContain(
'BIND(?operationUal AS ?ual)',
);
expect(captureQueries[0]!.match(
/<http:\/\/dkg\.io\/ontology\/shareOperationId> \?shareId/g,
)).toHaveLength(2);
expect(captureQueries[0]).toContain('LIMIT 100001');

// A second owner represents the next process start. It must load the
// durable capture rather than repeat the capture query. The late-entry
// marker read still runs on every start, so observe the sources directly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ export async function makeEntitySharePublisherFixture(options: {
rootEntity,
options.subGraphName,
));
// Preserve the recovery suite's manifest order independently of CONSTRUCT ordering.
const meta = result.quads.map(quad => ({ ...quad, graph: metaGraph }))
.sort((a, b) => sliceSubjects.indexOf(a.subject) - sliceSubjects.indexOf(b.subject));
const meta = result.quads.map(quad => ({ ...quad, graph: metaGraph }));
const decodedSlices = decodeEntityShareMetadata(options.contextGraphId, meta)
.filter(record => record.kind === 'slice');
const slices = options.rootEntities.map((rootEntity, index): EntitySharePublisherSlice => {
Expand Down
16 changes: 2 additions & 14 deletions packages/agent/test/_helpers/swm-recovery-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import {
contextGraphWorkspaceMetaGraphUri,
type OperationContext,
} from '@origintrail-official/dkg-core';
import type { WorkspacePublicSnapshotStore } from '@origintrail-official/dkg-publisher';
import { OxigraphStore, type Quad } from '@origintrail-official/dkg-storage';

import type { SyncPhase } from '../../src/sync/auth/request-build.js';
import type { SyncPageResult } from '../../src/sync/requester/page-fetch.js';
import { createSharedMemorySnapshotMaterializer } from
'../../src/sync/requester/swm-snapshot-materializer.js';
export { MemoryWorkspaceSnapshotStore as MemorySnapshotStore } from

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit: The re-export keeps a second name for the shared MemoryWorkspaceSnapshotStore.

Why it matters
An alias that only exists to avoid updating two import lists adds a naming indirection: readers must chase it to learn which store implementation a test uses, and future callers have no signal about which name is canonical.

Suggestion
Drop the alias (or re-export under the canonical name) and update the two importers (swm-recovery.test.ts, swm-recovery-revocation.test.ts) so the codebase has one name for the shared helper; folding the remaining local copies into the shared module can follow separately since they are pre-existing.

'./memory-workspace-snapshot-store.js';

export const CG = 'ws00-recovery';
export const WS = contextGraphWorkspaceGraphUri(CG);
Expand All @@ -25,19 +26,6 @@ export const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
export const UAL = 'did:dkg:hardhat:31337/0x00000000000000000000000000000000000000ab/7';
export const UAL_2 = 'did:dkg:hardhat:31337/0x00000000000000000000000000000000000000ab/8';

export class MemorySnapshotStore implements WorkspacePublicSnapshotStore {
readonly snapshots = new Map<string, Quad[]>();

async putSnapshot(input: { readonly digest: string; readonly quads: readonly Quad[] }) {
this.snapshots.set(input.digest, input.quads.map((quad) => ({ ...quad })));
return { ref: input.digest, byteLength: 0 };
}

async getSnapshot(ref: string): Promise<Quad[] | null> {
return this.snapshots.get(ref)?.map((quad) => ({ ...quad })) ?? null;
}
}

export function recoveryPage(quads: Quad[], completed = true): SyncPageResult {
return {
quads,
Expand Down
16 changes: 13 additions & 3 deletions packages/agent/test/entity-share-recovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ describe('entity-share recovery beside malformed KA heads', () => {
publish([root, siblingRoot], [...payload, ...siblingPayload]),
publish([root], payload, 'research'),
]);
const recoveryOrderedMeta = (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Suggestion: Manifest ordering was removed from the shared publisher fixture and re-derived inside one consumer.

Why it matters
Order-sensitive expectations that depend on incidental CONSTRUCT ordering are a classic source of flaky tests; keeping the ordering policy with the fixture data makes the dependency explicit and reusable.

Suggestion
Export the ordering from the fixture module (for example manifestOrderedMeta(fixture) or an opt-in ordered view) so the policy is defined once next to slices/sliceSubject and consumers opt in explicitly, and have the recovery test call that instead of re-deriving slice order locally.

fixture: Awaited<ReturnType<typeof publish>>,
): Quad[] => {
const sliceSubjects = fixture.slices.map((slice) => slice.sliceSubject);
return [...fixture.meta].sort(
(a, b) => sliceSubjects.indexOf(a.subject) - sliceSubjects.indexOf(b.subject),
);
};
const sliceFor = (fixture: Awaited<ReturnType<typeof publish>>, rootEntity: string) => {
const slice = fixture.slices.find(candidate => candidate.rootEntity === rootEntity);
if (!slice) throw new Error(`Entity-share recovery fixture did not publish ${rootEntity}`);
Expand All @@ -61,9 +69,11 @@ describe('entity-share recovery beside malformed KA heads', () => {
}
const metaGraph = entityPublished.metaGraph;
const namedMetaGraph = namedPublished.metaGraph;
const entityMeta = entityPublished.meta;
const twoRootMeta = twoRootPublished.meta;
const namedMeta = namedPublished.meta;
// Recovery traversal owns its deterministic manifest order; the publisher
// fixture returns the publisher/store result without consumer policy.
const entityMeta = recoveryOrderedMeta(entityPublished);
const twoRootMeta = recoveryOrderedMeta(twoRootPublished);
const namedMeta = recoveryOrderedMeta(namedPublished);
const sliceSubject = entitySlice.sliceSubject;
const siblingSubject = siblingSlice.sliceSubject;
const ka = swmFixtures(COVERAGE_CG).manifest(1)[0]!;
Expand Down
10 changes: 9 additions & 1 deletion packages/agent/test/pca-v10-facade.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, it, expect, vi } from 'vitest';
import { ethers } from 'ethers';
import { DKGAgent } from '../src/index.js';
import { MockChainAdapter, NoChainAdapter } from '@origintrail-official/dkg-chain';
import {
MockChainAdapter,
NoChainAdapter,
PcaUnavailableError,
} from '@origintrail-official/dkg-chain';

async function makeAgent(chain: MockChainAdapter | NoChainAdapter): Promise<DKGAgent> {
return DKGAgent.create({
Expand Down Expand Up @@ -126,6 +130,10 @@ describe('DKGAgent V10 PCA facade', () => {
const legacyAgent = await makeAgent(legacyOnly);
await expect(legacyAgent.requestBrowserWalletRpc('eth_blockNumber', [])).resolves.toBe('0xlegacy');
expect(legacyRpc).toHaveBeenCalledWith('eth_blockNumber', []);

const unsupported = await makeAgent(new NoChainAdapter());
await expect(unsupported.requestPublishingConvictionRpc('eth_chainId', []))
.rejects.toBeInstanceOf(PcaUnavailableError);
});

it('getPublishingConvictionAgents delegates to the adapter (checksummed list)', async () => {
Expand Down
108 changes: 37 additions & 71 deletions packages/agent/test/rfc64-legacy-swm-boundary-v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,31 @@ describe('RFC-64 10.0.16 legacy SWM boundary', () => {

it('captures once, remains private by count, and retires only after explicit republish', async () => {
const root = await secureTempRoot(roots);
const heads = new Map<string, string[]>([
[META_GRAPH, [UAL_ONE]],
[SUBGRAPH_META_GRAPH, []],
]);
const store = fakeStore(heads);
const store = new OxigraphStore();
const listGraphs = vi.spyOn(store, 'listGraphs');
await store.insert(legacySwmBoundaryFixtureQuadsV1({
graph: META_GRAPH,
contextGraphId: CONTEXT_GRAPH_ID,
ual: UAL_ONE,
operation: 'urn:dkg:workspace-operation:first-capture',
head: { shareOperationId: 'first-capture' },
operationShareOperationIds: ['first-capture'],
}));
const firstOwner = {};
await initializeRfc64LegacySwmBoundaryV1(firstOwner, root, store);

expect(readRfc64LegacySwmBoundaryCountV1(firstOwner, CONTEXT_GRAPH_ID)).toBe(1);

// A later restart must load the immutable first-upgrade capture instead of
// silently classifying a new 10.0.16 share as historical.
heads.set(SUBGRAPH_META_GRAPH, [UAL_TWO]);
await store.insert(legacySwmBoundaryFixtureQuadsV1({
graph: SUBGRAPH_META_GRAPH,
contextGraphId: CONTEXT_GRAPH_ID,
ual: UAL_TWO,
operation: 'urn:dkg:workspace-operation:late-named',
head: { shareOperationId: 'late-named' },
operationShareOperationIds: ['late-named'],
}));
const restartedOwner = {};
await initializeRfc64LegacySwmBoundaryV1(restartedOwner, root, store);
expect(readRfc64LegacySwmBoundaryCountV1(restartedOwner, CONTEXT_GRAPH_ID)).toBe(1);
Expand All @@ -102,7 +114,7 @@ describe('RFC-64 10.0.16 legacy SWM boundary', () => {
const secondRestartOwner = {};
await initializeRfc64LegacySwmBoundaryV1(secondRestartOwner, root, store);
expect(readRfc64LegacySwmBoundaryCountV1(secondRestartOwner, CONTEXT_GRAPH_ID)).toBe(0);
expect(store.listGraphs).not.toHaveBeenCalled();
expect(listGraphs).not.toHaveBeenCalled();
});

it('persists an atomic post-capture legacy SHARE companion until that exact UAL is republished', async () => {
Expand Down Expand Up @@ -540,56 +552,33 @@ describe('RFC-64 10.0.16 legacy SWM boundary', () => {
expect(readRfc64LegacySwmBoundaryCountV1(owner, CONTEXT_GRAPH_ID)).toBe(0);
});

it('does not let named metadata graphs consume the root graph capture cap', async () => {
it('captures the root graph with one behavioral store read and no graph enumeration', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: The rewritten capture test drops the only guard that named subgraph metadata graphs cannot enter the legacy SWM capture

What's wrong
The deleted tests does not let named metadata graphs consume the root graph capture cap and uses one bounded fully correlated capture query were the only place asserting that the one-time RFC-64 capture query (a) filters to the canonical root workspace meta graph rather than per-subgraph .../_shared_memory_meta/<name> graphs and (b) stays bounded by LIMIT 100_001. The replacement test seeds a single root graph and then asserts the boundary count stays 1 after a restart — but at that point the durable capture file already exists, so initializeRfc64LegacySwmBoundaryV1 reads the capture and never re-runs the head query. The assertion therefore cannot observe the named-graph exclusion it appears to protect. Among the remaining suites, the live Blazegraph fixture only ever writes the root META_GRAPH, so no test now exercises a named subgraph metadata graph against the live capture query. This matters because the caps are fail-closed: if the capture query regressed to include named metadata graphs, an install with many subgraph lanes could exceed RFC64_LEGACY_SWM_META_GRAPH_LIMIT_V1/RFC64_LEGACY_SWM_HEAD_LIMIT_V1 and refuse to start (or scan far more of the store) rather than silently corrupting data — a startup-availability regression that would ship unnoticed.

Example
Current replacement test (line 306):

// capture file is written here
await initializeRfc64LegacySwmBoundaryV1(firstOwner, root, store);
expect(readRfc64LegacySwmBoundaryCountV1(firstOwner, CONTEXT_GRAPH_ID)).toBe(1);
// a named subgraph head is inserted only AFTER the capture exists...
await store.insert(legacySwmBoundaryFixtureQuadsV1({ graph: SUBGRAPH_META_GRAPH, ... }));
const restartedOwner = {};
await initializeRfc64LegacySwmBoundaryV1(restartedOwner, root, store);
expect(readRfc64LegacySwmBoundaryCountV1(restartedOwner, CONTEXT_GRAPH_ID)).toBe(1);

Expected observation that is now missing: initialize against a fresh persistence root with (i) a root-graph legacy head and (ii) 16_384+ named subgraph _shared_memory_meta/<name> heads, and assert the capture count is exactly 1 and the agent.rfc64.legacySwmBoundary.readHeads query still carries LIMIT 100001.

Suggested direction
Keep the behavioural simplification but restore one capture-path assertion set: a fresh-root initialize with a root head plus named subgraph metadata heads, asserting the captured count excludes the named graphs and that the emitted query remains bounded (e.g. source-filtered query text asserting LIMIT 100001 and the two shareOperationId ?shareId correlations, or an equivalent count-based proof). Splitting it into a small dedicated test avoids re-coupling the whole suite to query text.

Confidence note
Medium confidence: I could not execute the suites (no node_modules in this worktree), so the claim rests on reading the production capture path (captureRfc64LegacySwmBoundaryV1) and confirming the live fixture writes only META_GRAPH. The remaining live assertions (count === LEGACY_HEAD_COUNT with 20_000 share-mismatched heads and >100_000 historical operation rows) still cover the share-id correlation and bound behaviourally, so the residual risk is specifically the named-subgraph exclusion and the explicit bound pinning.

For Agents
Look at captureRfc64LegacySwmBoundaryV1 in packages/agent/src/rfc64/legacy-swm-boundary-v1.ts and the deleted tests at the old lines ~294-344 of packages/agent/test/rfc64-legacy-swm-boundary-v1.test.ts. Preserve the new real-OxigraphStore, no-listGraphs behaviour, and add/reinstate a capture-path case proving named subgraph metadata graphs do not consume the legacy boundary. The fixing test should fail if the FILTER(... CONCAT ...) canonical-meta-graph restriction or the LIMIT 100_001 bound is removed from the capture query.

const root = await secureTempRoot(roots);
const heads = new Map<string, string[]>([[META_GRAPH, [UAL_ONE]]]);
for (let index = 0; index < 16_384; index += 1) {
heads.set(
contextGraphSharedMemoryMetaUri(CONTEXT_GRAPH_ID, `named-${index}`),
[],
);
}
const store = fakeStore(heads);
const store = new OxigraphStore();
await store.insert(legacySwmBoundaryFixtureQuadsV1({
graph: META_GRAPH,
contextGraphId: CONTEXT_GRAPH_ID,
ual: UAL_ONE,
operation: 'urn:dkg:workspace-operation:bounded-read',
head: { shareOperationId: 'bounded-read' },
operationShareOperationIds: ['bounded-read'],
}));
const listGraphs = vi.spyOn(store, 'listGraphs');
const query = vi.spyOn(store, 'query');

const owner = {};
await initializeRfc64LegacySwmBoundaryV1(owner, root, store);

expect(readRfc64LegacySwmBoundaryCountV1(owner, CONTEXT_GRAPH_ID)).toBe(1);
expect(store.listGraphs).not.toHaveBeenCalled();
expect(store.query).toHaveBeenCalledWith(
expect.stringContaining('GRAPH ?metaGraph'),
expect.objectContaining({
source: 'agent.rfc64.legacySwmBoundary.readHeads',
}),
);
});

it('uses one bounded fully correlated capture query', async () => {
const root = await secureTempRoot(roots);
const store = fakeStore(new Map([[META_GRAPH, [UAL_ONE]]]));

await initializeRfc64LegacySwmBoundaryV1({}, root, store);

const captureQueryCalls = vi.mocked(store.query).mock.calls.filter(([, options]) => (
expect(listGraphs).not.toHaveBeenCalled();
const captureQueryCalls = query.mock.calls.filter(([, options]) => (
options?.source === 'agent.rfc64.legacySwmBoundary.readHeads'
));
expect(captureQueryCalls).toHaveLength(1);
const captureQuery = captureQueryCalls[0]![0];
expect(captureQuery).toContain(
'?operation <http://www.w3.org/1999/02/22-rdf-syntax-ns#type>',
);
expect(captureQuery).toContain(
'BIND(?operationUal AS ?ual)',
);
expect(captureQuery).toContain(
'?head <http://dkg.io/ontology/kaUal> ?ual ; <http://dkg.io/ontology/shareOperationId> ?shareId',
);
expect(captureQuery).toContain('LIMIT 100001');
expect(captureQuery).toContain(
'FILTER(STRENDS(STR(?head), "#dkg-swm-head"))',
);
expect(captureQuery).not.toContain('VALUES');
expect(captureQuery).not.toContain('queryHints#');
// The fail-closed head cap (legacy-swm-boundary-v1.ts: bindings.length >
// RFC64_LEGACY_SWM_HEAD_LIMIT_V1) is only reachable while the query fetches
// one row MORE than the limit, so pin that exact relationship.
expect(captureQueryCalls[0]![0]).toContain('LIMIT 100001');
});

it.each([
Expand Down Expand Up @@ -753,29 +742,6 @@ async function secureTempRoot(roots: string[]): Promise<string> {
return root;
}

function fakeStore(
headsByGraph: Map<string, string[]>,
): TripleStore {
return {
listGraphs: vi.fn(async () => [...headsByGraph.keys()]),
query: vi.fn(async (_sparql: string, options?: { source?: string }) => {
const rootHeads = headsByGraph.get(META_GRAPH) ?? [];
if (options?.source === 'agent.rfc64.legacySwmBoundary.readHeads') {
return {
type: 'bindings' as const,
bindings: rootHeads.map((ual) => ({
metaGraph: META_GRAPH,
head: `${ual}#dkg-swm-head`,
ual,
contextGraphId: `"${CONTEXT_GRAPH_ID}"`,
})),
};
}
return { type: 'bindings' as const, bindings: [] };
}),
} as unknown as TripleStore;
}

function captureBinding(
contextGraphId = CONTEXT_GRAPH_ID,
ual = UAL_ONE,
Expand Down
47 changes: 47 additions & 0 deletions packages/agent/test/sync-fetch-coalescing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,53 @@ describe('exact VM recovery lifecycle', () => {
}
});

it.each(['configured-store', 'storeless'] as const)(
'awaits strict membership reconciliation and propagates failure with a %s',
async (storeCase) => {
const membershipUpsert = vi.fn(async () => undefined);
const agent = await createAgentWithSend(
async () => new Uint8Array(0),
undefined,
storeCase === 'configured-store'
? {
loadAll: async () => [],
upsert: membershipUpsert,
delete: async () => undefined,
}
: undefined,
);
const reconciliation = deferred<void>();
const reconciliationFailure = new Error('responsibility reconciliation failed');
const reconcile = vi.spyOn(agent, 'reconcileRfc64CatalogResponsibilityV1')
.mockReturnValue(reconciliation.promise);
try {
let settled = false;
const strictWrite = agent.upsertContextGraphMember({
contextGraphId: `strict-membership-${storeCase}`,
principalType: 'node',
principalId: PEER_A,
status: 'active',
}, { strict: true });
const failure = strictWrite.then(
() => { settled = true; return undefined; },
(error: unknown) => { settled = true; return error; },
);

await vi.waitFor(() => expect(reconcile).toHaveBeenCalledOnce());
expect(membershipUpsert).toHaveBeenCalledTimes(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Nit: The storeless branch asserts a never-wired mock was not called, so part of the new test is vacuous

Why it matters
A vacuous assertion adds noise and can imply coverage that does not exist; the meaningful guarantees (awaiting reconciliation, propagating failure) are already asserted for both branches.

Suggestion
Remove the storeless-branch call-count assertion or assert the real observable (no membership store configured).

storeCase === 'configured-store' ? 1 : 0,
);
expect(settled).toBe(false);

reconciliation.reject(reconciliationFailure);
expect(await failure).toBe(reconciliationFailure);
} finally {
reconciliation.resolve();
await agent.stop().catch(() => {});
}
},
);

it('quarantines a physically active reconcile until shutdown is retried', async () => {
const timeoutDescriptor = Object.getOwnPropertyDescriptor(
DKGAgentBase,
Expand Down
47 changes: 47 additions & 0 deletions packages/chain/test/rpc-usage.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,53 @@ describe('RPC usage accounting — raw request counts EQUAL the server-received
});
});

it('gives canonical tracker attribution precedence over its legacy compatibility map', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: The "canonical attribution precedence" test can pass even if normalization ignores attributions and uses the legacy map.

What's wrong
normalizeRpcUsageWindow branches on the presence of attributions: when present it ignores the legacy ethCallByConsumer/ethGetLogsByConsumerAndEndpointSlot maps and rebuilds ethCallByConsumer from the attributions array. The new test feeds a window produced by RpcUsageTracker.drainWindow(), and that method builds attributions from ethCallByConsumer (rpc-usage.ts:438-465), so the two representations are always identical in a tracker-drained window. Since the assertions only check values that are the same either way, a regression that reverted normalization to reading the legacy map would still pass green — the test pins { 'token.balanceOf': 1 } regardless of which branch the implementation takes, which is exactly the "false confidence" failure mode for a test whose name claims precedence.

Example
The precedence shape is only observable when the two fields disagree. A window such as normalizeRpcUsageWindow({ byMethod: { eth_call: 3 }, ethCallByConsumer: { 'stale.legacy': 9 }, attributions: [{ method: 'eth_call', consumer: 'token.balanceOf', count: 3 }], lifetimeTotal: 3 }) must yield ethCallByConsumer: { 'token.balanceOf': 3 } (and drop stale.legacy). The added test never constructs that input, so an implementation returning { 'stale.legacy': 9 } would still pass every assertion in this test. A second unexercised shape is a window that carries an eth_getLogs attribution alongside ethCallByConsumer; with a legacy-map implementation that eth_getLogs attribution would silently disappear, and the test would not notice.

Suggested direction
Keep the tracker round-trip case for the "both fields present and agree" shape, and add a hand-built dual-format window whose legacy map and canonical attributions disagree (including at least one eth_getLogs attribution) and assert the canonical values win and the stale legacy entries are absent.

For Agents
In packages/chain/test/rpc-usage.unit.test.ts (around the new test at line 287), replace or extend the tracker-driven input with a hand-written window object that sets ethCallByConsumer to values that contradict attributions (e.g. legacy { 'stale.legacy': 9 } vs canonical [{ method: 'eth_call', consumer: 'token.balanceOf', count: 3 }]), optionally adding an eth_getLogs attribution. Preserve the existing tracker round-trip assertions. The test must fail if normalizeRpcUsageWindow prefers the legacy map; assert the normalized ethCallByConsumer equals the canonical consumer and that attributions still contains the eth_getLogs entry.

const tracker = new RpcUsageTracker(() => 'evm:31337');
withRpcUsageConsumer('token.balanceOf', () => tracker.record('eth_call'));

const dualFormatWindow = tracker.drainWindow();
expect(dualFormatWindow.ethCallByConsumer).toEqual({ 'token.balanceOf': 1 });
expect(dualFormatWindow.attributions).toEqual([
{ method: 'eth_call', consumer: 'token.balanceOf', count: 1 },
]);
const normalized = normalizeRpcUsageWindow(dualFormatWindow);
expect(normalized.ethCallByConsumer).toEqual({ 'token.balanceOf': 1 });
expect(normalized.attributions).toEqual([
{ method: 'eth_call', consumer: 'token.balanceOf', count: 1 },
]);

// A tracker-drained window carries both representations built from the
// SAME counters, so it can only ever show them agreeing. Precedence is
// only observable when they disagree — hand-build that case. Reverting
// normalizeRpcUsageWindow to the legacy branch yields
// { 'stale.legacy': 9 } here and drops the eth_getLogs attribution.
const contradictory = normalizeRpcUsageWindow({
byMethod: { eth_call: 3, eth_getLogs: 1 },
ethCallByConsumer: { 'stale.legacy': 9 },
ethGetLogsByConsumerAndEndpointSlot: { 'stale.legacy': { primary: 9 } },
attributions: [
{ method: 'eth_call', consumer: 'token.balanceOf', count: 3 },
{
method: 'eth_getLogs',
consumer: 'cg.authority.history',
endpointSlot: 'fallback_1',
count: 1,
},
],
lifetimeTotal: 4,
});
expect(contradictory.ethCallByConsumer).toEqual({ 'token.balanceOf': 3 });
expect(contradictory.attributions).toEqual([
{ method: 'eth_call', consumer: 'token.balanceOf', count: 3 },
{
method: 'eth_getLogs',
consumer: 'cg.authority.history',
endpointSlot: 'fallback_1',
count: 1,
},
]);
});

it('returns a concrete empty RPC usage window from the mock adapter', () => {
expect(new MockChainAdapter().drainRpcUsage()).toEqual({
byMethod: {},
Expand Down
Loading
Loading