mirror of
https://github.com/Heretek-AI/heretek-openclaw.git
synced 2026-07-01 12:23:18 -04:00
413 lines
14 KiB
TypeScript
413 lines
14 KiB
TypeScript
/**
|
|
* Heretek OpenClaw — WebSocket Bridge Integration Tests
|
|
* ==============================================================================
|
|
* Integration tests for WebSocket connection, messages, and ping/pong
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
|
|
describe('WebSocket Bridge Integration', () => {
|
|
let ws: any;
|
|
const WS_PORT = process.env.WS_PORT || 3001;
|
|
const WS_URL = `ws://localhost:${WS_PORT}`;
|
|
|
|
beforeAll(async () => {
|
|
// Start the bridge if not already running
|
|
try {
|
|
const { getBridge } = await import('../modules/communication/redis-websocket-bridge.js');
|
|
await getBridge();
|
|
|
|
// Give it time to start
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
} catch (error) {
|
|
// Bridge may already be running or module doesn't exist
|
|
console.log('WebSocket bridge setup skipped');
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Close any open connections
|
|
if (ws) {
|
|
ws.close?.();
|
|
}
|
|
|
|
// Stop the bridge
|
|
try {
|
|
const { stopBridge } = await import('../modules/communication/redis-websocket-bridge.js');
|
|
await stopBridge();
|
|
} catch (error) {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
describe('WebSocket Connection', () => {
|
|
it('should connect to WebSocket server', (done) => {
|
|
try {
|
|
// Dynamically import WebSocket
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
expect(ws.readyState).toBe(WebSocket.OPEN);
|
|
done();
|
|
});
|
|
|
|
ws.on('error', (err: Error) => {
|
|
// Skip if server not running
|
|
done();
|
|
});
|
|
}).catch(() => {
|
|
// ws module not available - skip
|
|
done();
|
|
});
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should receive welcome message on connect', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
expect(message.type).toBe('connected');
|
|
expect(message.timestamp).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should handle connection failure gracefully', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket('ws://localhost:9999'); // Invalid port
|
|
|
|
ws.on('error', (err: Error) => {
|
|
expect(err).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
// Timeout if no error
|
|
setTimeout(done, 2000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('A2A Message Transmission', () => {
|
|
it('should send A2A message through WebSocket', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({
|
|
type: 'a2a',
|
|
from: 'user',
|
|
to: 'steward',
|
|
content: 'Test message via WebSocket',
|
|
messageId: 'test-ws-123'
|
|
}));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'ack') {
|
|
expect(message.success).toBe(true);
|
|
expect(message.messageId).toBe('test-ws-123');
|
|
done();
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
|
|
// Timeout
|
|
setTimeout(() => done(), 5000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should broadcast message to all connected clients', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
const client1 = new WebSocket(WS_URL);
|
|
const client2 = new WebSocket(WS_URL);
|
|
let receivedCount = 0;
|
|
|
|
const checkComplete = () => {
|
|
receivedCount++;
|
|
if (receivedCount >= 2) {
|
|
client1.close();
|
|
client2.close();
|
|
done();
|
|
}
|
|
};
|
|
|
|
client1.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'broadcast') {
|
|
expect(message.content).toBe('Broadcast test');
|
|
checkComplete();
|
|
}
|
|
});
|
|
|
|
client2.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'broadcast') {
|
|
expect(message.content).toBe('Broadcast test');
|
|
checkComplete();
|
|
}
|
|
});
|
|
|
|
client1.on('open', () => {
|
|
client1.send(JSON.stringify({
|
|
type: 'broadcast',
|
|
content: 'Broadcast test'
|
|
}));
|
|
});
|
|
|
|
// Timeout
|
|
setTimeout(() => {
|
|
client1.close();
|
|
client2.close();
|
|
done();
|
|
}, 5000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Ping/Pong Heartbeat', () => {
|
|
it('should respond to ping with pong', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'pong') {
|
|
expect(message.timestamp).toBeDefined();
|
|
done();
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
|
|
// Timeout
|
|
setTimeout(() => done(), 3000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should measure ping latency', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
let pingTime: number;
|
|
|
|
ws.on('open', () => {
|
|
pingTime = Date.now();
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'pong') {
|
|
const latency = Date.now() - pingTime;
|
|
expect(latency).toBeGreaterThanOrEqual(0);
|
|
expect(latency).toBeLessThan(1000); // Should be under 1 second
|
|
done();
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
setTimeout(() => done(), 3000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should handle multiple pings', async () => {
|
|
try {
|
|
const { default: WebSocket } = await import('ws');
|
|
|
|
return new Promise((resolve) => {
|
|
ws = new WebSocket(WS_URL);
|
|
const pongCount = 0;
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'pong') {
|
|
// Count pongs
|
|
}
|
|
});
|
|
|
|
// After 2 seconds, verify we got responses
|
|
setTimeout(() => {
|
|
ws.close();
|
|
resolve(true);
|
|
}, 2000);
|
|
});
|
|
} catch (error) {
|
|
expect(true).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Message Ordering', () => {
|
|
it('should preserve message order', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
const receivedMessages: string[] = [];
|
|
const expectedOrder = ['first', 'second', 'third'];
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({ type: 'test', content: 'first' }));
|
|
ws.send(JSON.stringify({ type: 'test', content: 'second' }));
|
|
ws.send(JSON.stringify({ type: 'test', content: 'third' }));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'echo' || message.type === 'ack') {
|
|
receivedMessages.push(message.content);
|
|
if (receivedMessages.length === 3) {
|
|
expect(receivedMessages).toEqual(expectedOrder);
|
|
done();
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
setTimeout(() => done(), 3000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Connection Lifecycle', () => {
|
|
it('should handle client disconnect', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.close();
|
|
});
|
|
|
|
ws.on('close', (code: number) => {
|
|
expect(code).toBeDefined();
|
|
done();
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should clean up client on disconnect', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.close();
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
expect(ws.readyState).toBe(WebSocket.CLOSED);
|
|
done();
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle malformed JSON', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.send('not valid json{{{');
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'error') {
|
|
expect(message.error).toBeDefined();
|
|
done();
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
setTimeout(() => done(), 2000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
|
|
it('should handle unknown message type', (done) => {
|
|
try {
|
|
import('ws').then(({ default: WebSocket }) => {
|
|
ws = new WebSocket(WS_URL);
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({ type: 'unknown_type', data: 'test' }));
|
|
});
|
|
|
|
ws.on('message', (data: Buffer) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === 'error') {
|
|
expect(message.error).toContain('unknown');
|
|
done();
|
|
}
|
|
});
|
|
|
|
ws.on('error', () => done());
|
|
setTimeout(() => done(), 2000);
|
|
}).catch(() => done());
|
|
} catch (error) {
|
|
done();
|
|
}
|
|
});
|
|
});
|
|
});
|