a2a-x402-gateway/test.mjs
OpSpawn eb346e535f v2.1.1: Stats endpoint, improved payment flow, test coverage
- /stats endpoint with real-time payment analytics and session tracking
- Multi-chain payment support (Base + SKALE Europa)
- SIWx session management for repeat access
- Updated README with comprehensive feature docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:25:38 +00:00

395 lines
16 KiB
JavaScript

/**
* 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);