/ scripts / demo-platform-test.mjs
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();