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']);
  });
});