#!/usr/bin/env node /** * AgentHub J5 — Messaging Validation Test * Tests new WebSocket handlers: room:list and message:history * Also validates broadcast and persistence */ import { io } from 'socket.io-client'; const AGENTHUB_HOST = process.argv[2] || '192.168.9.23:3000'; const BASE_URL = `http://${AGENTHUB_HOST}`; const WS_URL = `http://${AGENTHUB_HOST}`; // ANSI colors const GREEN = '\x1b[32m'; const YELLOW = '\x1b[33m'; const RED = '\x1b[31m'; const CYAN = '\x1b[36m'; const NC = '\x1b[0m'; function step(msg) { console.log(`${YELLOW}▶ ${msg}${NC}`); } function success(msg) { console.log(`${GREEN}✓ ${msg}${NC}`); } function error(msg) { console.error(`${RED}✗ ${msg}${NC}`); process.exit(1); } function info(msg) { console.log(`${CYAN}ℹ ${msg}${NC}`); } // Helper to make HTTP requests async function request(method, path, body, headers = {}) { const url = `${BASE_URL}${path}`; const options = { method, headers: { 'Content-Type': 'application/json', ...headers, }, }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(url, options); const text = await response.text(); try { return JSON.parse(text); } catch { return text; } } // Main test flow async function main() { console.log('🚀 AgentHub J5 — Messaging Validation Test'); console.log('━'.repeat(70)); console.log(`Target: ${BASE_URL}`); console.log(''); let alexiaId, alanId, roomId, jwtAlexia, jwtAlan; let clientAlexia, clientAlan; try { // Step 1: Health check step('Step 1/10: Health check'); const health = await request('GET', '/healthz'); if (health.status !== 'ok') { error(`Health check failed: ${JSON.stringify(health)}`); } success('AgentHub is healthy'); console.log(''); // Step 2: Create Alexia (admin role needed to create rooms) step('Step 2/10: Create agent Alexia'); const alexia = await request('POST', '/api/v1/agents', { name: `alexia-j5-${Date.now()}`, displayName: 'Alexia', role: 'admin', }); alexiaId = alexia.id; if (!alexiaId) error(`Failed to create Alexia: ${JSON.stringify(alexia)}`); success(`Alexia created: ${alexiaId}`); console.log(''); // Step 3: Create Alan step('Step 3/10: Create agent Alan'); const alan = await request('POST', '/api/v1/agents', { name: `alan-j5-${Date.now()}`, displayName: 'Alan', role: 'agent', }); alanId = alan.id; if (!alanId) error(`Failed to create Alan: ${JSON.stringify(alan)}`); success(`Alan created: ${alanId}`); console.log(''); // Step 4: Generate API tokens step('Step 4/10: Generate API tokens'); const tokenAlexia = await request('POST', `/api/v1/agents/${alexiaId}/tokens`, {}); const tokenAlan = await request('POST', `/api/v1/agents/${alanId}/tokens`, {}); if (!tokenAlexia.secret || !tokenAlan.secret) { error('Failed to generate API tokens'); } success(`Tokens generated`); console.log(''); // Step 5: Exchange for JWTs step('Step 5/10: Exchange tokens for JWTs'); const sessionAlexia = await request('POST', '/api/v1/sessions', { apiToken: tokenAlexia.secret, }); const sessionAlan = await request('POST', '/api/v1/sessions', { apiToken: tokenAlan.secret, }); jwtAlexia = sessionAlexia.jwt; jwtAlan = sessionAlan.jwt; if (!jwtAlexia || !jwtAlan) error('Failed to get JWTs'); success('JWTs obtained'); console.log(''); // Step 6: Create test room step('Step 6/10: Create test room'); const room = await request( 'POST', '/api/v1/rooms', { slug: `test-j5-${Date.now()}`, name: 'Test J5 Room', members: [alexiaId, alanId], }, { 'x-agent-id': alexiaId }, ); roomId = room.id; if (!roomId) error(`Failed to create room: ${JSON.stringify(room)}`); success(`Room created: ${roomId}`); console.log(''); // Step 7: Connect WebSockets step('Step 7/10: Connect WebSockets'); await new Promise((resolve, reject) => { let connectedCount = 0; clientAlexia = io(`${WS_URL}/agents`, { auth: { jwt: jwtAlexia }, }); clientAlan = io(`${WS_URL}/agents`, { auth: { jwt: jwtAlan }, }); const onHello = () => { connectedCount++; if (connectedCount === 2) { success('Both agents connected to WebSocket'); console.log(''); resolve(); } }; clientAlexia.on('agent:hello-ack', onHello); clientAlan.on('agent:hello-ack', onHello); clientAlexia.on('connect_error', reject); clientAlan.on('connect_error', reject); setTimeout(() => reject(new Error('WebSocket connection timeout')), 10000); }); // Step 8: Test room:list handler step('Step 8/10: Test room:list handler (NEW)'); await new Promise((resolve, reject) => { clientAlexia.emit('room:list', {}, (ack) => { if (ack.error) { error(`room:list error: ${ack.error}`); return reject(new Error(ack.error)); } if (!Array.isArray(ack.rooms)) { error(`room:list response invalid: ${JSON.stringify(ack)}`); return reject(new Error('Invalid response')); } const ourRoom = ack.rooms.find((r) => r.id === roomId); if (!ourRoom) { error(`Room ${roomId} not found in list`); return reject(new Error('Room not in list')); } success(`room:list works — found ${ack.rooms.length} room(s)`); info(` → Room: ${ourRoom.name} (${ourRoom.slug})`); console.log(''); resolve(); }); setTimeout(() => reject(new Error('room:list timeout')), 5000); }); // Step 9: Send messages and test broadcast step('Step 9/10: Send messages and test broadcast'); const messageIds = []; // Alexia sends message 1 await new Promise((resolve, reject) => { const receivedBy = { alexia: false, alan: false }; const checkComplete = () => { if (receivedBy.alexia && receivedBy.alan) { success('Message 1 broadcast to both agents'); resolve(); } }; clientAlexia.on('message:new', (payload) => { if (payload.body === 'Hello from Alexia') { receivedBy.alexia = true; checkComplete(); } }); clientAlan.on('message:new', (payload) => { if (payload.body === 'Hello from Alexia') { receivedBy.alan = true; checkComplete(); } }); clientAlexia.emit( 'message:send', { roomId, body: 'Hello from Alexia', }, (ack) => { if (ack.error) return reject(new Error(ack.error)); messageIds.push(ack.messageId); }, ); setTimeout(() => reject(new Error('Message broadcast timeout')), 5000); }); // Alan sends message 2 await new Promise((resolve, reject) => { clientAlan.emit( 'message:send', { roomId, body: 'Hello from Alan', }, (ack) => { if (ack.error) return reject(new Error(ack.error)); messageIds.push(ack.messageId); success('Message 2 sent by Alan'); resolve(); }, ); setTimeout(() => reject(new Error('Message send timeout')), 5000); }); // Wait for persistence await new Promise((resolve) => setTimeout(resolve, 500)); info(` → ${messageIds.length} messages sent`); console.log(''); // Step 10: Test message:history handler step('Step 10/10: Test message:history handler (NEW)'); await new Promise((resolve, reject) => { clientAlan.emit( 'message:history', { roomId, limit: 10, }, (ack) => { if (ack.error) { error(`message:history error: ${ack.error}`); return reject(new Error(ack.error)); } if (!Array.isArray(ack.messages)) { error(`message:history response invalid: ${JSON.stringify(ack)}`); return reject(new Error('Invalid response')); } if (ack.messages.length < 2) { error(`Expected at least 2 messages, got ${ack.messages.length}`); return reject(new Error('Not enough messages in history')); } // Verify both messages are present const bodies = ack.messages.map((m) => m.body); const hasAlexiaMsg = bodies.includes('Hello from Alexia'); const hasAlanMsg = bodies.includes('Hello from Alan'); if (!hasAlexiaMsg || !hasAlanMsg) { error('Missing messages in history'); return reject(new Error('Messages not found in history')); } success(`message:history works — retrieved ${ack.messages.length} message(s)`); info(` → hasMore: ${ack.hasMore}, cursor: ${ack.cursor ? 'present' : 'null'}`); console.log(''); resolve(); }, ); setTimeout(() => reject(new Error('message:history timeout')), 5000); }); // Cleanup clientAlexia.disconnect(); clientAlan.disconnect(); console.log('━'.repeat(70)); success('All J5 tests passed! ✨'); console.log('━'.repeat(70)); console.log(''); console.log('Validated:'); console.log(' ✓ room:list WebSocket event'); console.log(' ✓ message:history WebSocket event'); console.log(' ✓ message:send with persistence'); console.log(' ✓ Broadcast to all room members'); console.log(' ✓ Message persistence in DB'); console.log(''); } catch (err) { if (clientAlexia) clientAlexia.disconnect(); if (clientAlan) clientAlan.disconnect(); error(`Test failed: ${err.message}`); } } main();