// Tests named after the acceptance items they prove (A1 to A6). Offline: every test passes its own rpc and fetchJson.
// The live checks run only with VIEWER_LIVE=1.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { loadAgent, httpRpc, httpFetchJson, resolveUri, decodeDataUri } from '../viewer.js';
import { REGISTRIES, KOGNAI_TEST_IDENTITY } from '../config.js';
import { encodeOwnerOf, encodeTokenURI, encodeReadAllFeedback } from '../abi.js';
const F = JSON.parse(readFileSync(new URL('./fixtures.json', import.meta.url), 'utf8'));
const IDR = REGISTRIES['base-sepolia'].identity;
const REP = REGISTRIES['base-sepolia'].reputation;
// A fake JSON-RPC: answers eth_call by (to, data). Unknown calls revert.
function fakeRpc(table) {
const calls = [];
const rpc = async (method, params) => {
calls.push({ method, params });
assert.equal(method, 'eth_call');
assert.equal(params[1], 'latest');
const key = `${params[0].to.toLowerCase()}:${params[0].data}`;
const v = table[key];
if (v === undefined) throw Object.assign(new Error('execution reverted'), { code: 3 });
if (v instanceof Error) throw v;
return v;
};
return { rpc, calls };
}
const k = (to, data) => `${to.toLowerCase()}:${data}`;
function agentTable(id, uri, { owner = F.owner, feedback, idr = IDR, rep = REP } = {}) {
const t = { [k(idr, encodeOwnerOf(id))]: owner, [k(idr, encodeTokenURI(id))]: uri };
if (feedback !== undefined) t[k(rep, encodeReadAllFeedback(id))] = feedback;
return t;
}
function fakeFetchJson(map) {
const urls = [];
const fetchJson = async (url) => {
urls.push(url);
if (!(url in map)) throw new Error(`HTTP 404 for ${url}`);
return map[url];
};
return { fetchJson, urls };
}
test('A1 an existing agent shows its owner, its tokenURI and its registration file', async () => {
const { rpc } = fakeRpc(agentTable(7, F.uriHttps));
const { fetchJson, urls } = fakeFetchJson({ 'https://agent.example/registration.json': F.registration });
const r = await loadAgent({ agentId: 7, network: 'base-sepolia', identityRegistry: IDR, rpc, fetchJson });
assert.equal(r.status, 'ok');
assert.equal(r.agentId, '7');
assert.equal(r.owner, '0x21fded74c901129977b8e28c2588595163e1e235');
assert.equal(r.tokenURI, 'https://agent.example/registration.json');
assert.deepEqual(urls, ['https://agent.example/registration.json']);
assert.equal(r.registration.name, 'Fixture Agent');
assert.equal(r.registration.description, 'An agent used in tests.');
assert.equal(r.registration.image, 'ipfs://bafyimg/logo.png');
assert.deepEqual(r.registration.endpoints.map((e) => e.name), ['A2A', 'MCP']);
assert.deepEqual(r.registration.supportedTrust, ['reputation', 'crypto-economic']);
assert.equal(r.feedback, null, 'no reputation registry given');
});
test('A1 the registration file also reads the newer services field', async () => {
const reg = { name: 'S', description: 'd', services: [{ name: 'web', endpoint: 'https://s.example' }] };
const { rpc } = fakeRpc(agentTable(8, F.uriHttps));
const r = await loadAgent({ agentId: '8', network: 'base-sepolia', identityRegistry: IDR, rpc, fetchJson: fakeFetchJson({ 'https://agent.example/registration.json': reg }).fetchJson });
assert.deepEqual(r.registration.endpoints, [{ name: 'web', endpoint: 'https://s.example', version: undefined }]);
});
test('A2 an ipfs:// tokenURI is fetched through the https gateway', async () => {
const { rpc } = fakeRpc(agentTable(9, F.uriIpfs));
const url = 'https://gw.example/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/agent.json';
const { fetchJson, urls } = fakeFetchJson({ [url]: F.registration });
const r = await loadAgent({ agentId: 9, network: 'base-sepolia', identityRegistry: IDR, rpc, fetchJson, ipfsGateway: 'https://gw.example/ipfs/' });
assert.equal(r.status, 'ok');
assert.equal(r.tokenURI.startsWith('ipfs://'), true);
assert.deepEqual(urls, [url]);
assert.equal(resolveUri('ipfs://ipfs/abc', 'https://g.example/ipfs'), 'https://g.example/ipfs/abc');
assert.equal(resolveUri('ipfs://abc'), 'https://ipfs.io/ipfs/abc');
});
test('A2 data:application/json tokenURIs, base64 or not, are decoded without a request', async () => {
for (const [uri, name] of [[F.uriB64, 'Data Agent ü'], [F.uriPlain, 'Plain Data Agent']]) {
const { rpc } = fakeRpc(agentTable(10, uri));
const { fetchJson, urls } = fakeFetchJson({});
const r = await loadAgent({ agentId: 10, network: 'base-sepolia', identityRegistry: IDR, rpc, fetchJson });
assert.equal(r.status, 'ok', r.error);
assert.equal(r.registration.name, name);
assert.equal(urls.length, 0, 'no request for data: URIs');
}
assert.deepEqual(decodeDataUri('data:application/json;charset=utf-8;base64,' + Buffer.from('{"a":1}').toString('base64')), { a: 1 });
assert.equal(decodeDataUri('https://x.example'), null);
});
test('A3 an agent that does not exist gives a clear empty state (revert or zero owner)', async () => {
const revert = await loadAgent({ agentId: 404, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc({}).rpc, fetchJson: fakeFetchJson({}).fetchJson });
assert.equal(revert.status, 'empty');
assert.match(revert.error, /No agent 404/);
const msgRevert = { [k(IDR, encodeOwnerOf(405))]: new Error('VM Exception: reverted with ERC721NonexistentToken') };
assert.equal((await loadAgent({ agentId: 405, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(msgRevert).rpc })).status, 'empty');
const zero = await loadAgent({ agentId: 11, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(agentTable(11, F.uriHttps, { owner: F.zeroOwner })).rpc, fetchJson: fakeFetchJson({}).fetchJson });
assert.equal(zero.status, 'empty');
assert.equal(zero.owner, null);
});
test('A3 an RPC or file failure gives a clear error state and nothing throws', async () => {
const rpcDown = { [k(IDR, encodeOwnerOf(12))]: Object.assign(new Error('rate limited'), { code: -32005 }) };
const a = await loadAgent({ agentId: 12, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(rpcDown).rpc });
assert.equal(a.status, 'error');
assert.match(a.error, /RPC call failed: rate limited/);
const b = await loadAgent({ agentId: 13, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(agentTable(13, F.uriHttps)).rpc, fetchJson: fakeFetchJson({}).fetchJson });
assert.equal(b.status, 'error');
assert.equal(b.owner, '0x21fded74c901129977b8e28c2588595163e1e235');
assert.match(b.error, /registration file could not be read/);
const c = await loadAgent({ agentId: 14, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(agentTable(14, F.uriEmpty)).rpc });
assert.equal(c.status, 'error');
const d = await loadAgent({ agentId: 15, network: 'base-sepolia', identityRegistry: IDR, rpc: async () => '0xnothex' });
assert.equal(d.status, 'error');
for (const bad of [undefined, null, {}, { agentId: 'abc', rpc: async () => '0x' }, { agentId: 1 }, { agentId: -1, rpc: async () => '0x' }]) {
const r = await loadAgent(bad);
assert.equal(r.status, 'error');
assert.equal(typeof r.error, 'string');
}
const brokenFile = await loadAgent({ agentId: 16, network: 'base-sepolia', identityRegistry: IDR, rpc: fakeRpc(agentTable(16, F.uriHttps)).rpc, fetchJson: async () => 'not an object' });
assert.equal(brokenFile.status, 'error');
});
test('A4 reputation entries are read and summarised, revoked ones left out', async () => {
const { rpc, calls } = fakeRpc(agentTable(17, F.uriB64, { feedback: F.feedback }));
const r = await loadAgent({ agentId: 17, network: 'base-sepolia', identityRegistry: IDR, reputationRegistry: REP, rpc, fetchJson: fakeFetchJson({}).fetchJson });
assert.equal(r.status, 'ok');
assert.equal(calls.at(-1).params[0].to, REP);
assert.equal(r.feedback.count, 3);
// 90, -5 and 87.50: the revoked 100 is not counted
assert.equal(r.feedback.average, Math.round(((90 - 5 + 87.5) / 3) * 100) / 100);
assert.equal(r.feedback.items.length, 3);
assert.deepEqual(r.feedback.items[0], { client: '0x2222222222222222222222222222222222222222', score: 87.5, tags: [], feedbackIndex: 1 });
assert.deepEqual(r.feedback.items[1].tags, ['speed', 'x402']);
assert.equal(r.feedback.items[1].score, -5);
});
test('A4 an agent without feedback says so', async () => {
const r = await loadAgent({ agentId: 18, network: 'base-sepolia', identityRegistry: IDR, reputationRegistry: REP, rpc: fakeRpc(agentTable(18, F.uriB64, { feedback: F.feedbackEmpty })).rpc });
assert.deepEqual(r.feedback, { count: 0, average: null, items: [] });
const page = readFileSync(new URL('../app.js', import.meta.url), 'utf8');
assert.match(page, /no feedback yet/);
const down = await loadAgent({ agentId: 19, network: 'base-sepolia', identityRegistry: IDR, reputationRegistry: REP, rpc: fakeRpc(agentTable(19, F.uriB64)).rpc });
assert.equal(down.status, 'ok', 'identity still shows when reputation fails');
assert.match(down.feedback.error, /Reputation could not be read/);
});
test('A5 config targets Base and Base Sepolia, and loadAgent uses the network registry by default', async () => {
assert.equal(REGISTRIES.base.identity, '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432');
assert.equal(REGISTRIES.base.reputation, '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63');
assert.equal(REGISTRIES['base-sepolia'].identity, '0x8004A818BFB912233c491871b3d84c89A494BD9e');
assert.equal(REGISTRIES['base-sepolia'].reputation, '0x8004B663056A597Dffe9eCcC1965A193B7388713');
for (const net of ['base', 'base-sepolia']) {
const idr = REGISTRIES[net].identity;
const r = await loadAgent({ agentId: 20, network: net, rpc: fakeRpc(agentTable(20, F.uriB64, { idr })).rpc });
assert.equal(r.status, 'ok', net);
}
});
const live = process.env.VIEWER_LIVE === '1';
test('A5 live: reads agents on Base and Base Sepolia and the Kognai Sepolia test identity', { skip: !live && 'set VIEWER_LIVE=1 to run' }, async () => {
const sepoliaId = process.env.KOGNAI_TEST_AGENT_ID || KOGNAI_TEST_IDENTITY.agentId || '1';
for (const [net, id] of [['base-sepolia', sepoliaId], ['base', '1']]) {
const cfg = REGISTRIES[net];
const r = await loadAgent({ agentId: id, network: net, identityRegistry: cfg.identity, reputationRegistry: cfg.reputation, rpc: httpRpc(cfg.rpcUrl), fetchJson: httpFetchJson() });
assert.equal(r.status, 'ok', `${net} ${id}: ${r.error}`);
assert.match(r.owner, /^0x[0-9a-f]{40}$/);
assert.ok(r.registration.name);
assert.equal(typeof r.feedback.count, 'number');
}
});
test('A6 the page is mobile friendly (viewport meta, usable at 360 px) and has light and dark', () => {
const html = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const css = readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.match(html, /<meta name="viewport" content="width=device-width, initial-scale=1">/);
assert.match(html, /<script type="module" src="app.js">/);
assert.match(css, /@media \(prefers-color-scheme: dark\)/);
assert.match(css, /@media \(max-width: 420px\)/);
assert.match(css, /overflow-wrap: anywhere/);
// No fixed width wider than 360 px anywhere in the layout
for (const m of css.matchAll(/(?:^|[;{\s])(?:min-)?width:\s*(\d+)px/g)) assert.ok(Number(m[1]) <= 360, `width ${m[1]}px`);
// Text people see: no long dashes
for (const f of ['../index.html', '../app.js', '../README.md']) {
assert.doesNotMatch(readFileSync(new URL(f, import.meta.url), 'utf8'), /[\u2013\u2014]/, f);
}
});
agentgate-source: code mirror on Cloudflare Pages. All repos