demo-platform-test.mjs
1 #!/usr/bin/env node 2 /** 3 * SwarmClaw Platform Demo & Integration Test 4 * 5 * Exercises the full platform lifecycle: 6 * 1. Auth verification 7 * 2. Agent CRUD (create researcher + builder + delegator) 8 * 3. Session lifecycle (create, chat, verify messages) 9 * 4. Task board operations (create, update status, comment) 10 * 5. Delegation configuration 11 * 6. Cleanup 12 */ 13 14 const BASE = process.env.SWARMCLAW_URL || 'http://localhost:3456'; 15 const KEY = process.env.SWARMCLAW_ACCESS_KEY || ''; 16 17 const created = { agents: [], sessions: [], tasks: [] }; 18 let passed = 0; 19 let failed = 0; 20 21 // ── Helpers ────────────────────────────────────────────── 22 23 async function api(method, path, body) { 24 const opts = { 25 method, 26 headers: { 'X-Access-Key': KEY, 'Content-Type': 'application/json' }, 27 }; 28 if (body) opts.body = JSON.stringify(body); 29 const res = await fetch(`${BASE}${path}`, opts); 30 const text = await res.text(); 31 let data; 32 try { data = JSON.parse(text); } catch { data = text; } 33 return { status: res.status, data, ok: res.ok }; 34 } 35 36 function assert(condition, label) { 37 if (condition) { 38 passed++; 39 console.log(` ✓ ${label}`); 40 } else { 41 failed++; 42 console.log(` ✗ ${label}`); 43 } 44 } 45 46 function section(title) { 47 console.log(`\n${'─'.repeat(50)}`); 48 console.log(` ${title}`); 49 console.log('─'.repeat(50)); 50 } 51 52 // ── 1. Auth ────────────────────────────────────────────── 53 54 async function testAuth() { 55 section('1. Authentication'); 56 const { status, data } = await api('GET', '/api/auth'); 57 assert(status === 200, `GET /api/auth → ${status}`); 58 assert(data.firstTime === false, `Server is configured (firstTime=${data.firstTime})`); 59 60 const valid = await api('POST', '/api/auth', { key: KEY }); 61 assert(valid.ok, `POST /api/auth with valid key → ${valid.status}`); 62 63 const invalid = await api('POST', '/api/auth', { key: 'wrong-key' }); 64 assert(invalid.status === 401, `POST /api/auth with bad key → ${invalid.status}`); 65 } 66 67 // ── 2. Agent CRUD ──────────────────────────────────────── 68 69 async function createAgents() { 70 section('2. Agent CRUD'); 71 72 // Discover available credentials to wire up real chat 73 const creds = await api('GET', '/api/credentials'); 74 const openaiCred = Object.entries(creds.data || {}).find(([, v]) => v.provider === 'openai'); 75 const credentialId = openaiCred ? openaiCred[0] : null; 76 if (credentialId) { 77 console.log(` → Found OpenAI credential: ${credentialId}`); 78 } else { 79 console.log(` ⊘ No OpenAI credential found — chat test will be limited`); 80 } 81 82 // Create a Researcher agent 83 const researcher = await api('POST', '/api/agents', { 84 name: '🔬 Demo Researcher', 85 description: 'Researches topics and gathers information for the team', 86 systemPrompt: 'You are a research specialist. When given a topic, provide concise, factual summaries in 1-2 sentences max.', 87 provider: 'openai', 88 model: 'gpt-4o', 89 credentialId, 90 tools: ['web_search'], 91 }); 92 assert(researcher.ok, `Created Researcher agent → ${researcher.data?.id?.slice(0, 8)}`); 93 if (researcher.data?.id) created.agents.push(researcher.data.id); 94 95 // Create a Builder agent 96 const builder = await api('POST', '/api/agents', { 97 name: '🔨 Demo Builder', 98 description: 'Writes code and builds features based on research findings', 99 systemPrompt: 'You are a code builder. Write clean, minimal code. Respond with code blocks when asked to build something.', 100 provider: 'openai', 101 model: 'gpt-4o', 102 credentialId, 103 tools: ['shell', 'file_read', 'file_write'], 104 }); 105 assert(builder.ok, `Created Builder agent → ${builder.data?.id?.slice(0, 8)}`); 106 if (builder.data?.id) created.agents.push(builder.data.id); 107 108 // Create a delegating agent that coordinates both 109 const delegator = await api('POST', '/api/agents', { 110 name: '🧠 Demo Delegator', 111 description: 'Coordinates the researcher and builder to complete complex tasks', 112 systemPrompt: 'You are a delegating agent. Break tasks into research and build phases. Delegate to the most suitable agent.', 113 provider: 'openai', 114 model: 'gpt-4o', 115 credentialId, 116 delegationEnabled: true, 117 delegationTargetMode: 'selected', 118 delegationTargetAgentIds: [researcher.data?.id, builder.data?.id].filter(Boolean), 119 }); 120 assert(delegator.ok, `Created Delegator agent → ${delegator.data?.id?.slice(0, 8)}`); 121 if (delegator.data?.id) created.agents.push(delegator.data.id); 122 123 // List agents and verify ours exist 124 const list = await api('GET', '/api/agents'); 125 const agentIds = Object.keys(list.data || {}); 126 const allFound = created.agents.every(id => agentIds.includes(id)); 127 assert(allFound, `All 3 demo agents appear in GET /api/agents (${agentIds.length} total)`); 128 129 // Update the researcher's description 130 if (created.agents[0]) { 131 const updated = await api('PUT', `/api/agents/${created.agents[0]}`, { 132 description: 'Researches topics with web search and provides structured findings', 133 }); 134 assert(updated.ok, `Updated Researcher description via PUT`); 135 } 136 137 return { researcherId: created.agents[0], builderId: created.agents[1], delegatorId: created.agents[2] }; 138 } 139 140 // ── 3. Sessions & Chat ────────────────────────────────── 141 142 async function testSessions(agentIds) { 143 section('3. Sessions & Chat'); 144 145 // Create a session linked to the researcher 146 const session = await api('POST', '/api/sessions', { 147 name: 'Demo Research Session', 148 agentId: agentIds.researcherId, 149 }); 150 assert(session.ok, `Created session → ${session.data?.id?.slice(0, 8)}`); 151 if (session.data?.id) created.sessions.push(session.data.id); 152 153 // List sessions 154 const list = await api('GET', '/api/sessions'); 155 assert(list.ok && list.data, `GET /api/sessions returned data`); 156 157 // Send a chat message and read SSE stream 158 if (session.data?.id) { 159 const sid = session.data.id; 160 console.log(` → Sending chat message to session ${sid.slice(0, 8)}...`); 161 162 try { 163 const controller = new AbortController(); 164 const timeout = setTimeout(() => controller.abort(), 30000); 165 166 const res = await fetch(`${BASE}/api/sessions/${sid}/chat`, { 167 method: 'POST', 168 headers: { 'X-Access-Key': KEY, 'Content-Type': 'application/json' }, 169 body: JSON.stringify({ message: 'What is SwarmClaw? Answer in exactly one sentence.' }), 170 signal: controller.signal, 171 }); 172 173 assert(res.ok, `POST /chat returned ${res.status}`); 174 175 // Read SSE stream 176 const reader = res.body.getReader(); 177 const decoder = new TextDecoder(); 178 let fullText = ''; 179 let gotDone = false; 180 let gotError = false; 181 let errorMsg = ''; 182 let eventCount = 0; 183 184 while (true) { 185 const { done, value } = await reader.read(); 186 if (done) break; 187 188 const chunk = decoder.decode(value, { stream: true }); 189 const lines = chunk.split('\n').filter(l => l.startsWith('data: ')); 190 191 for (const line of lines) { 192 try { 193 const evt = JSON.parse(line.slice(6)); 194 eventCount++; 195 if (evt.t === 'md' || evt.t === 'd') fullText += evt.text || ''; 196 if (evt.t === 'done') { gotDone = true; break; } 197 if (evt.t === 'err') { gotError = true; errorMsg = evt.text; break; } 198 } catch { /* partial JSON, skip */ } 199 } 200 if (gotDone || gotError) break; 201 } 202 203 clearTimeout(timeout); 204 assert(eventCount > 0, `Received ${eventCount} SSE events`); 205 if (gotDone) { 206 assert(true, `Stream completed with 'done' event`); 207 const preview = fullText.replace(/\n/g, ' ').slice(0, 100); 208 console.log(` Response: "${preview}${fullText.length > 100 ? '...' : ''}"`); 209 } else if (gotError) { 210 console.log(` SSE error (infrastructure OK, provider issue): ${errorMsg}`); 211 assert(true, `Stream responded with error event (infra working, provider: ${errorMsg.slice(0, 50)})`); 212 } else { 213 assert(false, `Stream ended without done or error event`); 214 } 215 } catch (err) { 216 assert(false, `Chat streaming failed: ${err.message}`); 217 } 218 } 219 220 // Rename session 221 if (created.sessions[0]) { 222 const renamed = await api('PUT', `/api/sessions/${created.sessions[0]}`, { 223 name: 'Demo Research Session (completed)', 224 }); 225 assert(renamed.ok, `Renamed session via PUT`); 226 } 227 228 return session.data?.id; 229 } 230 231 // ── 4. Task Board ──────────────────────────────────────── 232 233 async function testTasks(agentIds) { 234 section('4. Task Board'); 235 236 // Create a task 237 const task = await api('POST', '/api/tasks', { 238 title: 'Demo: Research SwarmClaw architecture', 239 description: 'Analyze the SwarmClaw codebase and produce a summary of the key architectural patterns used.', 240 status: 'backlog', 241 agentId: agentIds.researcherId, 242 }); 243 assert(task.ok, `Created task → ${task.data?.id?.slice(0, 8)}`); 244 if (task.data?.id) created.tasks.push(task.data.id); 245 246 // Create a second task 247 const task2 = await api('POST', '/api/tasks', { 248 title: 'Demo: Build a health-check endpoint', 249 description: 'Create a simple /api/health endpoint that returns system status.', 250 status: 'backlog', 251 agentId: agentIds.builderId, 252 }); 253 assert(task2.ok, `Created second task → ${task2.data?.id?.slice(0, 8)}`); 254 if (task2.data?.id) created.tasks.push(task2.data.id); 255 256 // Update task status 257 if (created.tasks[0]) { 258 const updated = await api('PUT', `/api/tasks/${created.tasks[0]}`, { 259 status: 'queued', 260 }); 261 assert(updated.ok, `Moved task to 'queued' status`); 262 } 263 264 // Add a comment to the task 265 if (created.tasks[0]) { 266 const commented = await api('PUT', `/api/tasks/${created.tasks[0]}`, { 267 appendComment: 'Demo test: this task was created and updated programmatically by the platform test script.', 268 }); 269 assert(commented.ok, `Added comment to task`); 270 } 271 272 // Read task back 273 if (created.tasks[0]) { 274 const read = await api('GET', `/api/tasks/${created.tasks[0]}`); 275 assert(read.ok, `GET /api/tasks/${created.tasks[0].slice(0, 8)} returned data`); 276 assert(read.data?.status === 'queued', `Task status is 'queued'`); 277 assert(read.data?.comments?.length > 0, `Task has ${read.data?.comments?.length} comment(s)`); 278 } 279 280 // List all tasks 281 const list = await api('GET', '/api/tasks'); 282 assert(list.ok, `GET /api/tasks lists ${Object.keys(list.data || {}).length} tasks`); 283 } 284 285 // ── 5. Delegation ──────────────────────────────────────── 286 287 async function testDelegationConfig(agentIds) { 288 section('5. Delegation'); 289 290 if (!agentIds.delegatorId) { 291 console.log(' ⊘ Skipping: no delegator agent created'); 292 return; 293 } 294 295 const read = await api('GET', `/api/agents/${agentIds.delegatorId}`); 296 assert(read.ok, `GET /api/agents/${agentIds.delegatorId.slice(0, 8)} returned data`); 297 assert(read.data?.delegationEnabled === true, 'Delegation is enabled on the delegator'); 298 assert(read.data?.delegationTargetMode === 'selected', 'Delegator target mode is selected'); 299 assert(Array.isArray(read.data?.delegationTargetAgentIds) && read.data.delegationTargetAgentIds.length === 2, 'Delegator target list contains the expected agents'); 300 } 301 302 // ── 6. Provider & Credential endpoints ────────────────── 303 304 async function testProviders() { 305 section('6. Providers & Credentials'); 306 307 const providers = await api('GET', '/api/providers'); 308 assert(providers.ok, `GET /api/providers → ${providers.status}`); 309 310 const creds = await api('GET', '/api/credentials'); 311 assert(creds.ok, `GET /api/credentials → ${creds.status}`); 312 313 // Check daemon status 314 const daemon = await api('GET', '/api/daemon'); 315 assert(daemon.ok, `GET /api/daemon → status=${daemon.data?.status || 'unknown'}`); 316 } 317 318 // ── 7. Cleanup ─────────────────────────────────────────── 319 320 async function cleanup() { 321 section('7. Cleanup'); 322 323 // Delete tasks 324 for (const id of created.tasks) { 325 const del = await api('DELETE', `/api/tasks/${id}`); 326 console.log(` → Deleted task ${id.slice(0, 8)} → ${del.status}`); 327 } 328 329 // Delete sessions 330 for (const id of created.sessions) { 331 const del = await api('DELETE', `/api/sessions/${id}`); 332 console.log(` → Deleted session ${id.slice(0, 8)} → ${del.status}`); 333 } 334 335 // Delete agents 336 for (const id of created.agents) { 337 const del = await api('DELETE', `/api/agents/${id}`); 338 console.log(` → Deleted agent ${id.slice(0, 8)} → ${del.status}`); 339 } 340 341 console.log(` ✓ Cleanup complete`); 342 } 343 344 // ── Main ───────────────────────────────────────────────── 345 346 async function main() { 347 console.log('╔══════════════════════════════════════════════════╗'); 348 console.log('║ SwarmClaw Platform Demo & Integration Test ║'); 349 console.log('╚══════════════════════════════════════════════════╝'); 350 console.log(` Server: ${BASE}`); 351 console.log(` Key: ${KEY.slice(0, 8)}...${KEY.slice(-4)}`); 352 353 if (!KEY) { 354 console.error('\n ERROR: Set SWARMCLAW_ACCESS_KEY env var'); 355 process.exit(1); 356 } 357 358 try { 359 await testAuth(); 360 const agentIds = await createAgents(); 361 await testSessions(agentIds); 362 await testTasks(agentIds); 363 await testDelegationConfig(agentIds); 364 await testProviders(); 365 await cleanup(); 366 } catch (err) { 367 console.error(`\n FATAL: ${err.message}`); 368 console.error(err.stack); 369 // Still try to clean up 370 try { await cleanup(); } catch { /* best effort */ } 371 } 372 373 console.log(`\n${'═'.repeat(50)}`); 374 console.log(` Results: ${passed} passed, ${failed} failed`); 375 console.log('═'.repeat(50)); 376 process.exit(failed > 0 ? 1 : 0); 377 } 378 379 main();