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
82 changes: 4 additions & 78 deletions packages/agent/test/rfc64-finalized-vm-agent-precommit-v1.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
import {
CONTEXT_GRAPH_SHARED_PROJECTION_ID_V1,
type AuthorCatalogScopeV1,
type ContextGraphPolicyV1,
type Digest32V1,
} from '@origintrail-official/dkg-core';
import { OxigraphStore } from '@origintrail-official/dkg-storage';
import { describe, expect, it, vi } from 'vitest';

import type { AcceptedRfc64CatalogAccessSnapshotV1 } from '../src/rfc64/catalog-access-policy-v1.js';
import { createRfc64FinalizedVmAgentPrecommitV1 } from '../src/rfc64/finalized-vm-agent-precommit-v1.js';
import type { Rfc64PublicCatalogNativeBeforeAppliedHeadCommitPlanV1 } from '../src/rfc64/public-catalog-native-receiver-v1.js';
import {
RFC64_VM_AUTHOR,
RFC64_VM_BLOCK_HASH,
RFC64_VM_CG_STORAGE,
RFC64_VM_CHAIN_ID,
RFC64_VM_CONTEXT_GRAPH_NAME,
RFC64_VM_KAV10,
RFC64_VM_KA_STORAGE,
RFC64_VM_NETWORK_ID,
RFC64_VM_ON_CHAIN_CONTEXT_GRAPH_ID,
RFC64_VM_POLICY_DIGEST,
} from './support/rfc64-finalized-vm-placement-fixture.js';
import {
rfc64FinalizedVmPrecommitOptions as baseOptions,
rfc64FinalizedVmPrecommitPlan as plan,
} from './support/rfc64-finalized-vm-precommit-fixture.js';

const CATALOG_HEAD_DIGEST = `0x${'91'.repeat(32)}` as Digest32V1;
const INVENTORY_DIGEST = `0x${'92'.repeat(32)}` as Digest32V1;

describe('RFC-64 finalized VM agent precommit', () => {
it('rejects when the cleartext catalog lane has no numeric on-chain binding', async () => {
Expand Down Expand Up @@ -101,66 +88,5 @@ describe('RFC-64 finalized VM agent precommit', () => {
});
});

function baseOptions() {
return {
acceptedPolicySnapshotForCatalogScope: () => acceptedPolicy(),
rpcEndpoints: ['http://127.0.0.1:8545'],
getOnChainContextGraphId: async () => RFC64_VM_ON_CHAIN_CONTEXT_GRAPH_ID,
getEvmChainId: async () => BigInt(RFC64_VM_CHAIN_ID),
getKnowledgeAssetStorageAddress: async () => RFC64_VM_KA_STORAGE,
getKnowledgeAssetsLifecycleAddress: async () => RFC64_VM_KAV10,
store: new OxigraphStore(),
} as const;
}

function plan(): Readonly<Rfc64PublicCatalogNativeBeforeAppliedHeadCommitPlanV1> {
return Object.freeze({
catalogScope: Object.freeze({
networkId: RFC64_VM_NETWORK_ID,
contextGraphId: RFC64_VM_CONTEXT_GRAPH_NAME,
governanceChainId: RFC64_VM_CHAIN_ID,
governanceContractAddress: RFC64_VM_CG_STORAGE,
ownershipTransitionDigest: null,
subGraphName: null,
authorAddress: RFC64_VM_AUTHOR,
era: '0',
bucketCount: '1',
} satisfies AuthorCatalogScopeV1),
catalogHeadDigest: CATALOG_HEAD_DIGEST,
inventoryDigest: INVENTORY_DIGEST,
rows: Object.freeze([]),
});
}

function acceptedPolicy(): AcceptedRfc64CatalogAccessSnapshotV1 {
const policy = Object.freeze({
networkId: RFC64_VM_NETWORK_ID,
contextGraphId: RFC64_VM_CONTEXT_GRAPH_NAME,
governanceChainId: RFC64_VM_CHAIN_ID,
governanceContractAddress: RFC64_VM_CG_STORAGE,
ownershipTransitionDigest: null,
era: '0',
version: '0',
previousPolicyDigest: null,
accessPolicy: 0,
publishPolicy: 1,
publishAuthority: null,
publishAuthorityAccountId: '0',
projectionId: CONTEXT_GRAPH_SHARED_PROJECTION_ID_V1,
administrativeDelegationDigest: null,
source: {
kind: 'finalized-chain',
chainId: RFC64_VM_CHAIN_ID,
contractAddress: RFC64_VM_CG_STORAGE,
blockNumber: '123',
blockHash: RFC64_VM_BLOCK_HASH,
},
effectiveAt: '1700000000000',
issuedAt: '1700000000000',
} satisfies ContextGraphPolicyV1);
return Object.freeze({
policy,
policyDigest: RFC64_VM_POLICY_DIGEST,
roster: null,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* The integration seam this change exists to fix.
*
* The chain-package tests prove that `snapshotStrictCurrentFinalizedEvmConfigV1`
* accepts a three-endpoint pool. They do not prove the thing the change claims:
* that RFC64's finalized-VM precommit — the only production caller — can now
* construct its snapshot scope on a shipped network. That claim was previously
* demonstrated nowhere. The existing precommit suite hardcodes ONE endpoint
* (`rfc64-finalized-vm-agent-precommit-v1.test.ts`), so it constructed fine both
* before and after; the defect lived exactly in the gap between the two suites.
*
* So this drives the REAL precommit with the REAL shipped pool
* (`resolveRpcUrls(chain.rpcUrl, chain.rpcUrls)` from `network/testnet.json`,
* three distinct origins) and stubs only the outermost boundary — global
* `fetch` — so the endpoints actually dialled are observable.
*
* Before the fix this rejected with "requires 1..2 distinct endpoints" and
* dialled nothing. Both assertions below therefore fail on the unfixed code: the
* first on the message, the second on an empty dial log.
*/
import { resolveRpcUrls } from '@origintrail-official/dkg-chain';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { createRfc64FinalizedVmAgentPrecommitV1 } from '../src/rfc64/finalized-vm-agent-precommit-v1.js';
import {
rfc64FinalizedVmPrecommitOptions,
rfc64FinalizedVmPrecommitPlan,
} from './support/rfc64-finalized-vm-precommit-fixture.js';

const REPO_ROOT = join(import.meta.dirname, '..', '..', '..');

/** Exactly what the daemon hands the precommit: primary PLUS backups. */
function shippedTestnetPool(): string[] {
const config = JSON.parse(
readFileSync(join(REPO_ROOT, 'network', 'testnet.json'), 'utf8'),
);
return resolveRpcUrls(config.chain.rpcUrl, config.chain.rpcUrls);
}

afterEach(() => {
vi.unstubAllGlobals();
});

describe('RFC-64 finalized VM precommit on a shipped RPC pool', () => {
it('constructs its snapshot scope and dials the selected endpoints', async () => {
const pool = shippedTestnetPool();
// Guards the fixture itself: if `network/testnet.json` ever ships two URLs,
// this test would silently stop covering the oversized-pool case.
expect(pool.length).toBe(3);
expect(new Set(pool.map((url) => new URL(url).origin)).size).toBe(3);

const dialled: string[] = [];
vi.stubGlobal('fetch', async (input: unknown) => {
dialled.push(String(input));
// Fail the transport, not the construction. Preflight exhausting every
// selected endpoint is the shortest deterministic path that still proves
// the scope was built and handed real URLs.
throw new Error('stubbed transport failure');
});

// Only `rpcEndpoints` varies — that is the whole point of this regression.
const precommit = createRfc64FinalizedVmAgentPrecommitV1(
rfc64FinalizedVmPrecommitOptions({ rpcEndpoints: pool }),
);

// The precommit still fails — there is no chain behind the stub — but it must
// fail at the TRANSPORT, downstream of scope construction. Every selected
// endpoint failing preflight retryably surfaces the last retryable failure,
// which is the JSON-RPC one; the pre-fix failure was a TypeError about the
// endpoint count, raised before any dial happened at all.
await expect(
precommit(rfc64FinalizedVmPrecommitPlan(), new AbortController().signal),
).rejects.toThrow(/JSON-RPC eth_chainId transport failed/i);

// The load-bearing assertion: selection reached the wire. Two of the three
// shipped endpoints were dialled, in configuration order.
//
// Compared as URLs, not as raw strings: the config stores endpoints after
// `new URL(...)` normalization, so `https://sepolia.base.org` arrives at
// `fetch` with a trailing slash. Comparing `href` on both sides keeps the
// assertion about WHICH endpoint was dialled instead of quietly restating
// the normalizer's spelling rules.
const href = (url: string) => new URL(url).href;
expect([...new Set(dialled)]).toEqual([href(pool[0]!), href(pool[1]!)]);
// And the third is stranded — stated as a tested fact rather than left as a
// silent consequence. Selection is health-blind and configuration-ordered,
// so `base-sepolia.drpc.org` is never reached by a strict finalized read even
// when the primary is degraded. Tracked as a follow-up, not fixed here.
expect(dialled).not.toContain(href(pool[2]!));
});
});


120 changes: 120 additions & 0 deletions packages/agent/test/support/rfc64-finalized-vm-precommit-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Shared fixtures for the RFC-64 finalized-VM agent precommit tests.
*
* Extracted because two suites need the same plan, accepted-policy snapshot and
* base options while varying one field each — the shipped-pool regression only
* varies `rpcEndpoints`. Keeping a second copy meant a change to the plan or
* policy shape required synchronized edits across files before either suite's
* actual assertion could run.
*
* The digests are exported so a caller can assert against them rather than
* re-deriving the literals.
*/
import {
CONTEXT_GRAPH_SHARED_PROJECTION_ID_V1,
type AuthorCatalogScopeV1,
type ContextGraphPolicyV1,
type Digest32V1,
} from '@origintrail-official/dkg-core';
import { OxigraphStore } from '@origintrail-official/dkg-storage';

import type { AcceptedRfc64CatalogAccessSnapshotV1 } from '../../src/rfc64/catalog-access-policy-v1.js';
import type { Rfc64FinalizedVmAgentPrecommitOptionsV1 } from '../../src/rfc64/finalized-vm-agent-precommit-v1.js';
import type { Rfc64PublicCatalogNativeBeforeAppliedHeadCommitPlanV1 } from '../../src/rfc64/public-catalog-native-receiver-v1.js';
import {
RFC64_VM_AUTHOR,
RFC64_VM_BLOCK_HASH,
RFC64_VM_CG_STORAGE,
RFC64_VM_CHAIN_ID,
RFC64_VM_CONTEXT_GRAPH_NAME,
RFC64_VM_KAV10,
RFC64_VM_KA_STORAGE,
RFC64_VM_NETWORK_ID,
RFC64_VM_ON_CHAIN_CONTEXT_GRAPH_ID,
RFC64_VM_POLICY_DIGEST,
} from './rfc64-finalized-vm-placement-fixture.js';

export const RFC64_VM_CATALOG_HEAD_DIGEST = `0x${'91'.repeat(32)}` as Digest32V1;
export const RFC64_VM_INVENTORY_DIGEST = `0x${'92'.repeat(32)}` as Digest32V1;

/** The before-applied-head commit plan the precommit is driven with. */
export function rfc64FinalizedVmPrecommitPlan():
Readonly<Rfc64PublicCatalogNativeBeforeAppliedHeadCommitPlanV1> {
return Object.freeze({
catalogScope: Object.freeze({
networkId: RFC64_VM_NETWORK_ID,
contextGraphId: RFC64_VM_CONTEXT_GRAPH_NAME,
governanceChainId: RFC64_VM_CHAIN_ID,
governanceContractAddress: RFC64_VM_CG_STORAGE,
ownershipTransitionDigest: null,
subGraphName: null,
authorAddress: RFC64_VM_AUTHOR,
era: '0',
bucketCount: '1',
} satisfies AuthorCatalogScopeV1),
catalogHeadDigest: RFC64_VM_CATALOG_HEAD_DIGEST,
inventoryDigest: RFC64_VM_INVENTORY_DIGEST,
rows: Object.freeze([]),
});
}

/** One accepted, public, finalized-chain policy snapshot. */
export function acceptedRfc64VmPolicySnapshot(): AcceptedRfc64CatalogAccessSnapshotV1 {
const policy = Object.freeze({
networkId: RFC64_VM_NETWORK_ID,
contextGraphId: RFC64_VM_CONTEXT_GRAPH_NAME,
governanceChainId: RFC64_VM_CHAIN_ID,
governanceContractAddress: RFC64_VM_CG_STORAGE,
ownershipTransitionDigest: null,
era: '0',
version: '0',
previousPolicyDigest: null,
accessPolicy: 0,
publishPolicy: 1,
publishAuthority: null,
publishAuthorityAccountId: '0',
projectionId: CONTEXT_GRAPH_SHARED_PROJECTION_ID_V1,
administrativeDelegationDigest: null,
source: {
kind: 'finalized-chain',
chainId: RFC64_VM_CHAIN_ID,
contractAddress: RFC64_VM_CG_STORAGE,
blockNumber: '123',
blockHash: RFC64_VM_BLOCK_HASH,
},
effectiveAt: '1700000000000',
issuedAt: '1700000000000',
} satisfies ContextGraphPolicyV1);
return Object.freeze({
policy,
policyDigest: RFC64_VM_POLICY_DIGEST,
roster: null,
});
}

/**
* Base precommit options. Each suite overrides the one field it is about — a
* single resolver for the noncanonical-input cases, `rpcEndpoints` for the
* shipped-pool regression.
*
* A fresh `OxigraphStore` per call: sharing one across tests would let state
* from an earlier case leak into a later assertion.
*
* Overrides are `Partial<Rfc64FinalizedVmAgentPrecommitOptionsV1>` rather than a
* loose record, so a misspelled key or a wrongly-shaped value is a type error at
* the call site that introduces it, not a silently-ignored property.
*/
export function rfc64FinalizedVmPrecommitOptions(
overrides: Partial<Rfc64FinalizedVmAgentPrecommitOptionsV1> = {},
): Rfc64FinalizedVmAgentPrecommitOptionsV1 {
return {
acceptedPolicySnapshotForCatalogScope: () => acceptedRfc64VmPolicySnapshot(),
rpcEndpoints: ['http://127.0.0.1:8545'],
getOnChainContextGraphId: async () => RFC64_VM_ON_CHAIN_CONTEXT_GRAPH_ID,
getEvmChainId: async () => BigInt(RFC64_VM_CHAIN_ID),
getKnowledgeAssetStorageAddress: async () => RFC64_VM_KA_STORAGE,
getKnowledgeAssetsLifecycleAddress: async () => RFC64_VM_KAV10,
store: new OxigraphStore(),
...overrides,
};
}
1 change: 1 addition & 0 deletions packages/agent/vitest.rfc64-unit-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const RFC64_UNIT_TESTS = [
"test/rfc64-finalized-vm-composer-v1.test.ts",
"test/rfc64-finalized-vm-runtime-v1.test.ts",
"test/rfc64-finalized-vm-agent-precommit-v1.test.ts",
"test/rfc64-finalized-vm-precommit-shipped-pool.test.ts",
"test/rfc64-agent-inventory-lifecycle.test.ts",
"test/rfc64-author-catalog-producer.test.ts",
"test/rfc64-control-object-store-v1.test.ts",
Expand Down
Loading
Loading