File size: 9,071 Bytes
ec8acdf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | import { describe, it, before } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const ROOT = resolve(dirname(__filename), '..');
const CARD_PATH = join(ROOT, 'public/.well-known/agent-card.json');
const SERVER_CARD_PATH = join(ROOT, 'public/.well-known/mcp/server-card.json');
const VERCEL_JSON_PATH = join(ROOT, 'vercel.json');
const card = JSON.parse(readFileSync(CARD_PATH, 'utf-8'));
const serverCard = JSON.parse(readFileSync(SERVER_CARD_PATH, 'utf-8'));
const vercelConfig = JSON.parse(readFileSync(VERCEL_JSON_PATH, 'utf-8'));
// Guards for the A2A surface (orank Identity `a2a-agent-card`): the card at
// /.well-known/agent-card.json and the JSON-RPC endpoint at /a2a must stay
// deployable as a pair β a card pointing at a dead endpoint is exactly the
// phantom-endpoint failure mode the 2026-07 MCP-discovery saga was about.
describe('a2a: agent card contract', () => {
it('carries the required A2A v0.3.0 fields', () => {
assert.equal(card.protocolVersion, '0.3.0');
assert.ok(card.name && typeof card.name === 'string');
assert.ok(card.description && card.description.length > 50, 'description must be substantive');
assert.equal(card.url, 'https://www.worldmonitor.app/a2a');
assert.equal(card.preferredTransport, 'JSONRPC');
assert.ok(card.version && typeof card.version === 'string');
assert.ok(Array.isArray(card.defaultInputModes) && card.defaultInputModes.length > 0);
assert.ok(Array.isArray(card.defaultOutputModes) && card.defaultOutputModes.length > 0);
});
it('declares capabilities honestly: no streaming, no push notifications, no tasks', () => {
assert.equal(card.capabilities.streaming, false);
assert.equal(card.capabilities.pushNotifications, false);
assert.equal(card.capabilities.stateTransitionHistory, false);
assert.equal(card.supportsAuthenticatedExtendedCard, false);
});
it('every skill has id, name, description, and tags', () => {
assert.ok(Array.isArray(card.skills) && card.skills.length >= 2);
for (const skill of card.skills) {
assert.ok(skill.id, 'skill missing id');
assert.ok(skill.name, `${skill.id} missing name`);
assert.ok(skill.description && skill.description.length > 0, `${skill.id} missing description`);
assert.ok(Array.isArray(skill.tags) && skill.tags.length > 0, `${skill.id} missing tags`);
}
});
it('shares branding with the MCP server card (same icon)', () => {
assert.equal(card.iconUrl, serverCard.icon);
});
it('vercel.json routes /a2a to the endpoint and shields it from the SPA catch-all', () => {
const rewrite = vercelConfig.rewrites.find((r) => r.source === '/a2a');
assert.ok(rewrite, 'missing /a2a rewrite');
assert.equal(rewrite.destination, '/api/a2a');
const catchAll = vercelConfig.rewrites.find((r) => r.destination === '/dashboard.html' && r.source.startsWith('/((?!'));
assert.ok(catchAll, 'dashboard catch-all rewrite missing');
assert.ok(catchAll.source.includes('a2a'), 'a2a must be excluded from the dashboard catch-all');
const corsBlock = vercelConfig.headers.find((h) => h.source === '/a2a');
assert.ok(corsBlock, 'missing /a2a headers block');
const acao = corsBlock.headers.find((h) => h.key === 'Access-Control-Allow-Origin');
assert.equal(acao?.value, '*');
});
});
describe('a2a: JSON-RPC endpoint', () => {
let handler;
let suggestTools;
before(async () => {
const mod = await import(`../api/a2a.ts?t=${Date.now()}`);
handler = mod.default;
suggestTools = mod.suggestTools;
});
function post(body) {
return handler(
new Request('https://worldmonitor.app/a2a', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: typeof body === 'string' ? body : JSON.stringify(body),
}),
);
}
function rpc(method, params, id = 1) {
return post({ jsonrpc: '2.0', id, method, params });
}
it('message/send routes a chokepoint query to get_chokepoint_status', async () => {
const res = await rpc('message/send', {
message: {
role: 'user',
messageId: 'm-1',
parts: [{ kind: 'text', text: 'Which tool gives live shipping chokepoint status?' }],
},
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.jsonrpc, '2.0');
assert.equal(body.id, 1);
assert.ok(body.result, `expected result, got ${JSON.stringify(body.error)}`);
assert.equal(body.result.kind, 'message');
assert.equal(body.result.role, 'agent');
assert.ok(body.result.messageId, 'response message must carry a messageId');
const textPart = body.result.parts.find((p) => p.kind === 'text');
const dataPart = body.result.parts.find((p) => p.kind === 'data');
assert.ok(textPart?.text.length > 0, 'must include a text part');
assert.ok(dataPart, 'must include a data part');
const names = dataPart.data.suggestedTools.map((t) => t.name);
assert.ok(names.includes('get_chokepoint_status'), `expected get_chokepoint_status in ${names}`);
assert.ok(dataPart.data.howToCall.mcp.endpoint.endsWith('/mcp'));
});
it('message/send accepts the pre-0.3 part dialect ({type: "text"})', async () => {
const res = await rpc('message/send', {
message: { role: 'user', parts: [{ type: 'text', text: 'country risk scores' }] },
});
const body = await res.json();
assert.ok(body.result, `expected result, got ${JSON.stringify(body.error)}`);
assert.ok(body.result.parts.some((p) => p.kind === 'data'));
});
it('message/send answers freshness queries with the public freshness envelope', async () => {
const res = await rpc('message/send', {
message: { role: 'user', parts: [{ kind: 'text', text: 'Is the market data fresh right now?' }] },
});
const body = await res.json();
assert.ok(body.result, `expected result, got ${JSON.stringify(body.error)}`);
const dataPart = body.result.parts.find((p) => p.kind === 'data');
assert.ok(dataPart.data.freshness, 'freshness envelope must be attached');
assert.ok('stale' in dataPart.data.freshness, 'freshness envelope must carry stale');
});
it('message/send echoes a provided contextId', async () => {
const res = await rpc('message/send', {
message: { role: 'user', contextId: 'ctx-42', parts: [{ kind: 'text', text: 'sanctions data' }] },
});
const body = await res.json();
assert.equal(body.result.contextId, 'ctx-42');
});
it('message/send without a text part β -32602', async () => {
const res = await rpc('message/send', { message: { role: 'user', parts: [{ kind: 'file' }] } });
const body = await res.json();
assert.equal(body.error.code, -32602);
});
it('unsupported optional methods β -32004; tasks β -32001; extended card β -32007; unknown β -32601', async () => {
for (const [method, code] of [
['message/stream', -32004],
['tasks/resubscribe', -32004],
['tasks/pushNotificationConfig/set', -32004],
['tasks/get', -32001],
['tasks/cancel', -32001],
['agent/getAuthenticatedExtendedCard', -32007],
['bogus/method', -32601],
]) {
const res = await rpc(method, {});
const body = await res.json();
assert.equal(body.error?.code, code, `${method} must answer ${code}`);
}
});
it('malformed JSON β -32700; non-2.0 envelope β -32600', async () => {
const bad = await post('{not json');
assert.equal((await bad.json()).error.code, -32700);
const wrong = await post({ id: 1, method: 'message/send' });
assert.equal((await wrong.json()).error.code, -32600);
});
it('GET β 405 with Allow header; OPTIONS β 204 with CORS', async () => {
const get = await handler(new Request('https://worldmonitor.app/a2a', { method: 'GET' }));
assert.equal(get.status, 405);
assert.equal(get.headers.get('Allow'), 'POST, OPTIONS');
const options = await handler(new Request('https://worldmonitor.app/a2a', { method: 'OPTIONS' }));
assert.equal(options.status, 204);
assert.equal(options.headers.get('Access-Control-Allow-Origin'), '*');
});
it('responses are JSON, uncacheable, and CORS-open', async () => {
const res = await rpc('bogus/method', {});
assert.match(res.headers.get('Content-Type'), /application\/json/);
assert.equal(res.headers.get('Cache-Control'), 'no-store');
assert.equal(res.headers.get('Access-Control-Allow-Origin'), '*');
});
it('suggestTools: empty/stopword-only queries return no suggestions', () => {
assert.deepEqual(suggestTools(''), []);
assert.deepEqual(suggestTools('what can you give'), []);
});
it('card skill ids match the behaviours the endpoint implements', () => {
const ids = card.skills.map((s) => s.id).sort();
assert.deepEqual(ids, ['check-data-freshness', 'route-to-tool']);
});
});
|