erc8004-identity-viewer / tree / test/viewer.test.js · raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// 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