Implements missing WebSocket event handlers for J5 messaging: - room:list: List all rooms for the authenticated agent - message:history: Retrieve paginated message history for a room Also adds: - Unit tests for both handlers in socket.test.ts - E2E validation script test/j5-messaging-validation.js Completes BARAAA-50 deliverables for real-time messaging. Co-Authored-By: Paperclip <noreply@paperclip.ing>
348 lines
9.7 KiB
JavaScript
348 lines
9.7 KiB
JavaScript
#!/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
|
||
step('Step 2/10: Create agent Alexia');
|
||
const alexia = await request('POST', '/api/v1/agents', {
|
||
name: `alexia-j5-${Date.now()}`,
|
||
displayName: 'Alexia',
|
||
role: 'agent',
|
||
});
|
||
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/tokens', {
|
||
agentId: alexiaId,
|
||
});
|
||
const tokenAlan = await request('POST', '/api/v1/tokens', {
|
||
agentId: alanId,
|
||
});
|
||
if (!tokenAlexia.token || !tokenAlan.token) {
|
||
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.token,
|
||
});
|
||
const sessionAlan = await request('POST', '/api/v1/sessions', {
|
||
apiToken: tokenAlan.token,
|
||
});
|
||
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();
|