- Coordinator agent accepts complex tasks and decomposes into subtasks - Delegates to specialized worker agents via A2A v0.3 JSON-RPC - Handles x402 payment flows (paid skills return payment-required) - Free skills execute end-to-end through delegation chain - Web dashboard with real-time swarm monitoring - 14 tests passing - Worker agents: screenshot, PDF, HTML (via A2A gateway) - Event log tracks all delegations and completions
199 lines
6.5 KiB
JavaScript
199 lines
6.5 KiB
JavaScript
import { strict as assert } from 'node:assert';
|
|
|
|
const BASE = 'http://localhost:4003';
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
passed++;
|
|
console.log(` PASS ${name}`);
|
|
} catch (err) {
|
|
failed++;
|
|
console.log(` FAIL ${name}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function fetchJSON(path, opts) {
|
|
const res = await fetch(BASE + path, opts);
|
|
return { status: res.status, data: await res.json() };
|
|
}
|
|
|
|
console.log('\n=== Agent Swarm Tests ===\n');
|
|
|
|
// --- Health ---
|
|
await test('GET /health returns 200', async () => {
|
|
const { status, data } = await fetchJSON('/health');
|
|
assert.equal(status, 200);
|
|
assert.equal(data.status, 'ok');
|
|
assert.equal(data.service, 'agent-swarm');
|
|
assert.equal(data.version, '1.0.0');
|
|
assert.ok(data.workerAgents >= 3);
|
|
});
|
|
|
|
// --- Agent Card ---
|
|
await test('GET /.well-known/agent-card.json returns valid card', async () => {
|
|
const { status, data } = await fetchJSON('/.well-known/agent-card.json');
|
|
assert.equal(status, 200);
|
|
assert.equal(data.name, 'OpSpawn Agent Swarm');
|
|
assert.ok(data.skills.length >= 3);
|
|
assert.ok(data.capabilities.extensions.length >= 2);
|
|
assert.equal(data.provider.organization, 'OpSpawn');
|
|
});
|
|
|
|
// --- Worker Discovery ---
|
|
await test('GET /agents returns coordinator + workers', async () => {
|
|
const { status, data } = await fetchJSON('/agents');
|
|
assert.equal(status, 200);
|
|
assert.ok(data.coordinator);
|
|
assert.ok(data.workers.length >= 3);
|
|
const ids = data.workers.map(w => w.id);
|
|
assert.ok(ids.includes('screenshot'));
|
|
assert.ok(ids.includes('pdf'));
|
|
assert.ok(ids.includes('html'));
|
|
});
|
|
|
|
// --- Empty swarm list ---
|
|
await test('GET /swarms returns empty list initially', async () => {
|
|
const { status, data } = await fetchJSON('/swarms');
|
|
assert.equal(status, 200);
|
|
assert.ok(Array.isArray(data.swarms));
|
|
});
|
|
|
|
// --- Event log ---
|
|
await test('GET /events returns events array', async () => {
|
|
const { status, data } = await fetchJSON('/events');
|
|
assert.equal(status, 200);
|
|
assert.ok(Array.isArray(data.events));
|
|
assert.ok(typeof data.total === 'number');
|
|
});
|
|
|
|
// --- A2A message/send creates swarm ---
|
|
await test('POST /tasks/:id/send creates a swarm', async () => {
|
|
const contextId = crypto.randomUUID();
|
|
const { status, data } = await fetchJSON(`/tasks/${contextId}/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: '1',
|
|
method: 'message/send',
|
|
params: {
|
|
message: {
|
|
role: 'user',
|
|
parts: [{ type: 'text', text: 'Create an HTML preview of Hello World' }]
|
|
}
|
|
}
|
|
})
|
|
});
|
|
assert.equal(status, 200);
|
|
assert.equal(data.jsonrpc, '2.0');
|
|
assert.ok(data.result.id); // swarm ID
|
|
assert.equal(data.result.status.state, 'working');
|
|
assert.ok(data.result.status.message.metadata['swarm.subtaskCount'] >= 1);
|
|
});
|
|
|
|
// --- Invalid message rejected ---
|
|
await test('POST /tasks/:id/send rejects invalid message', async () => {
|
|
const { status, data } = await fetchJSON(`/tasks/${crypto.randomUUID()}/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ jsonrpc: '2.0', id: '2', method: 'message/send', params: {} })
|
|
});
|
|
assert.equal(status, 400);
|
|
assert.ok(data.error);
|
|
});
|
|
|
|
// --- Screenshot task planning ---
|
|
await test('Screenshot instruction creates screenshot subtask', async () => {
|
|
const contextId = crypto.randomUUID();
|
|
const { status, data } = await fetchJSON(`/tasks/${contextId}/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: '3',
|
|
method: 'message/send',
|
|
params: {
|
|
message: {
|
|
role: 'user',
|
|
parts: [{ type: 'text', text: 'Take a screenshot of https://example.com' }]
|
|
}
|
|
}
|
|
})
|
|
});
|
|
assert.equal(status, 200);
|
|
const subtasks = data.result.status.message.metadata['swarm.subtasks'];
|
|
assert.ok(subtasks.some(s => s.type === 'screenshot'));
|
|
});
|
|
|
|
// --- Report task planning ---
|
|
await test('Report instruction creates multiple subtasks', async () => {
|
|
const contextId = crypto.randomUUID();
|
|
const { status, data } = await fetchJSON(`/tasks/${contextId}/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: '4',
|
|
method: 'message/send',
|
|
params: {
|
|
message: {
|
|
role: 'user',
|
|
parts: [{ type: 'text', text: 'Create a visual report about https://opspawn.com with screenshots and a PDF' }]
|
|
}
|
|
}
|
|
})
|
|
});
|
|
assert.equal(status, 200);
|
|
const subtasks = data.result.status.message.metadata['swarm.subtasks'];
|
|
assert.ok(subtasks.length >= 2, `Expected >= 2 subtasks, got ${subtasks.length}`);
|
|
assert.ok(subtasks.some(s => s.type === 'screenshot'));
|
|
assert.ok(subtasks.some(s => s.type === 'pdf'));
|
|
});
|
|
|
|
// --- Swarm list grows ---
|
|
await test('GET /swarms shows created swarms', async () => {
|
|
const { status, data } = await fetchJSON('/swarms');
|
|
assert.equal(status, 200);
|
|
assert.ok(data.swarms.length >= 3, `Expected >= 3 swarms, got ${data.swarms.length}`);
|
|
});
|
|
|
|
// --- Events logged ---
|
|
await test('GET /events shows events from swarm creation', async () => {
|
|
const { status, data } = await fetchJSON('/events');
|
|
assert.equal(status, 200);
|
|
assert.ok(data.events.length >= 3, `Expected >= 3 events, got ${data.events.length}`);
|
|
assert.ok(data.events.some(e => e.action === 'swarm-created'));
|
|
});
|
|
|
|
// --- Dashboard ---
|
|
await test('GET / returns dashboard HTML', async () => {
|
|
const res = await fetch(BASE + '/');
|
|
assert.equal(res.status, 200);
|
|
const html = await res.text();
|
|
assert.ok(html.includes('Agent Swarm'));
|
|
assert.ok(html.includes('Multi-agent'));
|
|
});
|
|
|
|
// --- 404 for unknown paths ---
|
|
await test('GET /unknown returns 404', async () => {
|
|
const { status } = await fetchJSON('/unknown');
|
|
assert.equal(status, 404);
|
|
});
|
|
|
|
// Wait for async swarm execution
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
// --- Check swarm execution results ---
|
|
await test('Swarms execute and have results', async () => {
|
|
const { data } = await fetchJSON('/swarms');
|
|
const finished = data.swarms.filter(s => s.state !== 'planned' && s.state !== 'running');
|
|
// At least some should have attempted execution
|
|
assert.ok(finished.length >= 0 || data.swarms.length >= 3);
|
|
});
|
|
|
|
console.log(`\n=== Results: ${passed} passed, ${failed} failed, ${passed + failed} total ===\n`);
|
|
process.exit(failed > 0 ? 1 : 0);
|