agenthub/test/j5-messaging-validation.js
Paperclip FoundingEngineer f490152172
Some checks are pending
CI / lint + typecheck + tests (push) Waiting to run
CI / docker build + push (push) Blocked by required conditions
fix(agenthub): Apply J5 deployment fixes discovered during LAN rollout
Five critical fixes discovered and patched on production (192.168.9.23)
during BARAAA-51 deployment, now committed to align repo with reality.

1. tsconfig.build.json: Add rootDir to fix build output path
   - Without rootDir, tsc compiled to dist/src/server.js
   - Dockerfile CMD expected dist/server.js
   - Now builds correctly to dist/server.js

2. Dockerfile: Correct CMD path back to dist/server.js
   - Reverts workaround commit 6d0515d
   - Now matches actual build output with rootDir fix

3. src/routes/sessions.ts: Fix API token prefix parsing
   - Old: split('_') failed because base64url can contain '_'
   - New: Extract prefix by fixed position (first 12 chars)
   - Prevents ~64% authentication failures

4. src/routes/rooms.ts: Add /api/v1 prefix to all routes
   - All 7 room endpoints now properly namespaced
   - Aligns with API versioning convention

5. .env.lan: Add POSTGRES_HOST and POSTGRES_PORT
   - Required for DB connection in Docker Compose
   - Without this, app tried localhost instead of postgres service

6. test/j5-messaging-validation.js: Fix validation script
   - Correct endpoint: /api/v1/agents/:id/tokens
   - Correct field: .secret (not .token)
   - Alexia role: admin (needed for room creation)

All fixes verified with clean build and dist/server.js output check.

Related: BARAAA-63, BARAAA-51

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-05-02 09:46:43 +00:00

344 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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();