/** * Test suite for A2A x402 Gateway v2 */ const BASE = 'http://localhost:4002'; let passed = 0, failed = 0; async function test(name, fn) { try { await fn(); console.log(` PASS: ${name}`); passed++; } catch (err) { console.log(` FAIL: ${name} - ${err.message}`); failed++; } } function assert(c, m) { if (!c) throw new Error(m || 'Assertion failed'); } console.log('\nA2A x402 Gateway v2 Tests\n'); await test('GET /health returns ok', async () => { const r = await fetch(`${BASE}/health`); const d = await r.json(); assert(d.status === 'ok'); assert(d.uptime > 0); }); await test('GET /.well-known/agent-card.json returns valid V2 agent card', async () => { const r = await fetch(`${BASE}/.well-known/agent-card.json`); assert(r.status === 200); const d = await r.json(); assert(d.name === 'OpSpawn Screenshot Agent'); assert(d.version === '2.1.0', `Version: ${d.version}`); assert(d.skills.length === 3); assert(d.skills[0].id === 'screenshot'); assert(d.protocolVersion === '0.3.0'); assert(d.provider.organization === 'OpSpawn'); assert(d.capabilities.stateTransitionHistory === true); // V2: extension with payment config const payExt = d.extensions.find(e => e.uri === 'urn:x402:payment:v2'); assert(payExt, 'Has V2 payment extension'); assert(payExt.config.version === '2.0', 'Extension version 2.0'); assert(payExt.config.networks.length >= 2, `Networks: ${payExt.config.networks.length}`); assert(payExt.config.features.includes('siwx'), 'Supports SIWx'); }); await test('GET /x402 returns V2 service catalog', async () => { const r = await fetch(`${BASE}/x402`); const d = await r.json(); assert(d.version === '2.1.0', `Version: ${d.version}`); assert(d.protocols.a2a.version === '0.3.0'); assert(d.protocols.x402.version === '2.0', `x402 version: ${d.protocols.x402.version}`); assert(d.protocols.x402.networks.length >= 2, 'Has multiple networks'); assert(d.protocols.x402.networks[0].network === 'eip155:8453', 'Base CAIP-2 ID'); assert(d.protocols.x402.features.siwx, 'SIWx feature documented'); assert(d.endpoints.length === 3); assert(d.endpoints[0].price === '$0.01'); assert(d.endpoints[2].price === 'free'); }); await test('GET /api/info returns V2 agent info with stats', async () => { const r = await fetch(`${BASE}/api/info`); const d = await r.json(); assert(d.agent.name === 'OpSpawn Screenshot Agent'); assert(d.payments.version === '2.0', 'Payment version 2.0'); assert(d.payments.networks.length >= 2, 'Multiple networks'); assert(d.payments.features.includes('siwx'), 'SIWx feature'); assert(d.stats.uptime > 0); assert(typeof d.stats.siwxSessions === 'number', 'SIWx session count'); assert(d.stats.paymentsByType, 'Has payment breakdown'); }); await test('GET /api/siwx returns session list', async () => { const r = await fetch(`${BASE}/api/siwx`); const d = await r.json(); assert(Array.isArray(d.sessions), 'Has sessions array'); assert(typeof d.total === 'number', 'Has total count'); }); await test('GET /dashboard returns HTML page', async () => { const r = await fetch(`${BASE}/dashboard`); const t = await r.text(); assert(t.includes('A2A x402 Gateway')); assert(t.includes('x402 V2')); assert(t.includes('SIWx')); assert(t.includes('SKALE')); assert(t.includes('Agent Skills')); assert(t.includes('Payment Flow')); }); await test('A2A message/send: free markdown-to-html works', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-free', method: 'message/send', params: { message: { messageId: 'msg-free', role: 'user', kind: 'message', parts: [{ kind: 'text', text: '# Test Heading\n\nHello world' }], }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result, 'Has result'); assert(d.result.status.state === 'completed', `State: ${d.result.status.state}`); const msg = d.result.status.message; assert(msg.parts.length >= 2, 'Has text and data parts'); const dataPart = msg.parts.find(p => p.kind === 'data'); assert(dataPart, 'Has data part'); assert(dataPart.data.html.includes('Test Heading'), 'HTML has heading'); }); await test('A2A message/send: screenshot returns V2 payment-required', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-paid', method: 'message/send', params: { message: { messageId: 'msg-paid', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }], }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result, 'Has result'); assert(d.result.status.state === 'input-required', `State: ${d.result.status.state}`); const msg = d.result.status.message; assert(msg.parts[0].text.includes('Payment required'), 'Payment required message'); const dataPart = msg.parts.find(p => p.kind === 'data'); assert(dataPart.data['x402.payment.required'] === true, 'x402 flag'); assert(dataPart.data['x402.version'] === '2.0', `x402 version: ${dataPart.data['x402.version']}`); // V2: accepts array with CAIP-2 network IDs const accepts = dataPart.data['x402.accepts']; assert(Array.isArray(accepts), 'Accepts is array'); assert(accepts.length >= 2, `Networks: ${accepts.length}`); assert(accepts[0].network === 'eip155:8453', `Base CAIP-2: ${accepts[0].network}`); assert(accepts[0].price === '$0.01', `Price: ${accepts[0].price}`); // SIWx extension const exts = dataPart.data['x402.extensions']; assert(exts?.['sign-in-with-x']?.supported === true, 'SIWx supported'); }); await test('A2A message/send: PDF returns V2 payment-required', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-pdf', method: 'message/send', params: { message: { messageId: 'msg-pdf', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Convert to PDF: # My Document' }], }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result.status.state === 'input-required', `State: ${d.result.status.state}`); const dataPart = d.result.status.message.parts.find(p => p.kind === 'data'); assert(dataPart.data['x402.accepts'][0].price === '$0.005', `Price: ${dataPart.data['x402.accepts'][0].price}`); }); await test('A2A message/send: paid screenshot with payment payload', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-paid-exec', method: 'message/send', params: { message: { messageId: 'msg-paid-exec', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }], metadata: { 'x402.payment.payload': { scheme: 'exact', network: 'eip155:8453', signature: '0xfake', from: '0xTestWallet123' }, 'x402.payer': '0xTestWallet123', }, }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result, 'Has result'); const state = d.result.status.state; assert(state === 'completed' || state === 'failed', `State: ${state}`); if (state === 'completed') { assert(d.result.metadata['x402.version'] === '2.0', 'V2 metadata'); assert(d.result.metadata['x402.siwx.active'] === true, 'SIWx session created'); const filePart = d.result.status.message.parts.find(p => p.kind === 'file'); assert(filePart, 'Has file part (screenshot)'); assert(filePart.mimeType === 'image/png', 'PNG mime type'); console.log(` (Screenshot: ${Math.round(filePart.data.length * 3/4 / 1024)}KB)`); } else { console.log(` (Expected: SnapAPI may not be running: ${d.result.status.message.parts[0].text})`); } }); await test('SIWx: session recorded after payment', async () => { const r = await fetch(`${BASE}/api/siwx`); const d = await r.json(); // After the paid screenshot test, the wallet should be in sessions const session = d.sessions.find(s => s.wallet === '0xtestwallet123'); assert(session, `SIWx session found for test wallet (sessions: ${d.total})`); assert(session.skills.includes('screenshot'), 'Screenshot skill recorded'); }); await test('SIWx: session access bypasses payment', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-siwx', method: 'message/send', params: { message: { messageId: 'msg-siwx', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }], metadata: { 'x402.siwx.wallet': '0xTestWallet123' }, }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result, 'Has result'); // Should execute directly (completed or failed) without payment-required const state = d.result.status.state; assert(state === 'completed' || state === 'failed', `SIWx access state: ${state} (should not be input-required)`); assert(state !== 'input-required', 'SIWx should bypass payment'); }); await test('SIWx: unknown wallet still requires payment', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-siwx-unknown', method: 'message/send', params: { message: { messageId: 'msg-siwx-unknown', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }], metadata: { 'x402.siwx.wallet': '0xUnknownWallet' }, }, configuration: { blocking: true }, }, }), }); const d = await r.json(); assert(d.result.status.state === 'input-required', `Unknown wallet should require payment: ${d.result.status.state}`); }); await test('A2A tasks/get returns task', async () => { const r1 = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'create', method: 'message/send', params: { message: { messageId: 'msg-get', role: 'user', kind: 'message', parts: [{ kind: 'text', text: '# Test' }] }, configuration: { blocking: true } }, }), }); const d1 = await r1.json(); const taskId = d1.result.id; const r2 = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'get', method: 'tasks/get', params: { id: taskId } }), }); const d2 = await r2.json(); assert(d2.result.id === taskId, 'Task ID matches'); assert(d2.result.status.state === 'completed', 'Task completed'); }); await test('A2A tasks/get for unknown task returns error', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'notfound', method: 'tasks/get', params: { id: 'nonexistent' } }), }); const d = await r.json(); assert(d.error, 'Has error'); assert(d.error.code === -32001, 'Task not found error code'); }); await test('A2A tasks/cancel works', async () => { // Create a paid task (input-required state) const r1 = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'cancel-create', method: 'message/send', params: { message: { messageId: 'msg-cancel', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }] } }, }), }); const d1 = await r1.json(); const taskId = d1.result.id; const r2 = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'cancel', method: 'tasks/cancel', params: { id: taskId } }), }); const d2 = await r2.json(); assert(d2.result.status.state === 'canceled', 'Task canceled'); }); await test('Invalid JSON-RPC returns error', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '1.0', id: 'bad', method: 'message/send', params: {} }), }); const d = await r.json(); assert(d.error, 'Has error'); assert(d.error.code === -32600, 'Invalid request error'); }); await test('Unknown method returns error', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'unknown', method: 'nonexistent/method', params: {} }), }); const d = await r.json(); assert(d.error.code === -32601, 'Method not found'); }); await test('GET /api/payments reflects activity', async () => { const r = await fetch(`${BASE}/api/payments`); const d = await r.json(); assert(d.total > 0, `Total payments: ${d.total}`); assert(d.payments.some(p => p.type === 'payment-required'), 'Has payment-required entry'); }); await test('SKALE Europa: correct chain ID in agent card', async () => { const r = await fetch(`${BASE}/.well-known/agent-card.json`); const d = await r.json(); const payExt = d.extensions.find(e => e.uri === 'urn:x402:payment:v2'); const skaleNet = payExt.config.networks.find(n => n.name === 'SKALE Europa'); assert(skaleNet, 'SKALE Europa network present'); assert(skaleNet.network === 'eip155:2046399126', `SKALE Europa CAIP-2: ${skaleNet.network}`); assert(skaleNet.gasless === true, 'SKALE is gasless'); assert(skaleNet.tokenAddress === '0x5F795bb52dAC3085f578f4877D450e2929D2F13d', `SKALE USDC: ${skaleNet.tokenAddress}`); }); await test('SKALE Europa: correct USDC in payment requirements', async () => { const r = await fetch(BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 'test-skale-pay', method: 'message/send', params: { message: { messageId: 'msg-skale', role: 'user', kind: 'message', parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }], }, }, }), }); const d = await r.json(); const accepts = d.result.status.message.parts.find(p => p.kind === 'data')?.data['x402.accepts']; const skaleAccept = accepts.find(a => a.network === 'eip155:2046399126'); assert(skaleAccept, 'SKALE Europa in payment accepts'); assert(skaleAccept.asset === '0x5F795bb52dAC3085f578f4877D450e2929D2F13d', `SKALE USDC asset: ${skaleAccept.asset}`); assert(skaleAccept.gasless === true, 'Marked gasless'); // Base should use different USDC address const baseAccept = accepts.find(a => a.network === 'eip155:8453'); assert(baseAccept.asset !== skaleAccept.asset, 'Base and SKALE use different USDC addresses'); }); await test('SKALE Europa: x402 catalog shows correct chain details', async () => { const r = await fetch(`${BASE}/x402`); const d = await r.json(); const skaleNet = d.protocols.x402.networks.find(n => n.name === 'SKALE Europa'); assert(skaleNet, 'SKALE Europa in catalog'); assert(skaleNet.chainId === 2046399126, `Chain ID: ${skaleNet.chainId}`); assert(skaleNet.gasless === true, 'Gasless flag'); assert(skaleNet.network === 'eip155:2046399126', 'CAIP-2 format'); }); console.log(`\nResults: ${passed} passed, ${failed} failed, ${passed + failed} total\n`); process.exit(failed > 0 ? 1 : 0);