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>
344 lines
9.7 KiB
JavaScript
344 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 (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();
|