mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
f13eb30ce1
Phase 4 Implementation: - SignalingServer: WebSocket server for WebRTC peer discovery - AgentSpawnAPI: REST API for spawning QE agents from dashboard - EdgeServer: Combined HTTP + WebSocket server entry point CLI Enhancements: - Added `aqe agent spawn <type>` command with task execution - Added `aqe agent list` to view spawned agents - Agent state persists to .agentic-qe/agents/ directory P2PService Fixes (from brutal honesty review): - Integrated real SignalingClient for WebRTC signaling - Added RTCPeerConnection management with ICE handling - Replaced mock connection with actual WebRTC setup - Fixed PatternCategory enum mapping Dashboard Fixes: - Added error state display instead of silent failures - Shows red error banner when agent spawn fails Tests: - AgentSpawnAPI.test.ts (17 tests) - SignalingServer.test.ts (7 tests) - Webapp component tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
261 lines
7.5 KiB
TypeScript
261 lines
7.5 KiB
TypeScript
/**
|
|
* Signaling Server Tests
|
|
*
|
|
* Tests for the WebSocket signaling server that handles WebRTC peer discovery.
|
|
*
|
|
* @module tests/edge/server/SignalingServer.test
|
|
*/
|
|
|
|
import { WebSocket } from 'ws';
|
|
import { SignalingServer, SignalingServerConfig } from '../../../src/edge/server/SignalingServer';
|
|
import { SignalingMessageType, generateId } from '../../../src/edge/p2p/webrtc/types';
|
|
|
|
describe('SignalingServer', () => {
|
|
let server: SignalingServer;
|
|
const port = 9999;
|
|
const serverUrl = `ws://localhost:${port}`;
|
|
|
|
beforeAll(async () => {
|
|
server = new SignalingServer({ port });
|
|
await server.start();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await server.stop();
|
|
});
|
|
|
|
describe('Connection Management', () => {
|
|
it('should accept WebSocket connections with peerId', async () => {
|
|
const peerId = `peer-${Date.now()}`;
|
|
const ws = new WebSocket(`${serverUrl}?peerId=${peerId}`);
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
ws.on('open', () => {
|
|
expect(ws.readyState).toBe(WebSocket.OPEN);
|
|
ws.close();
|
|
resolve();
|
|
});
|
|
ws.on('error', reject);
|
|
});
|
|
});
|
|
|
|
it('should reject connections without peerId', async () => {
|
|
const ws = new WebSocket(serverUrl);
|
|
|
|
await new Promise<void>((resolve) => {
|
|
ws.on('close', (code) => {
|
|
expect(code).toBe(4000);
|
|
resolve();
|
|
});
|
|
ws.on('error', () => {
|
|
// Expected for rejected connection
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should replace existing connections from same peerId', async () => {
|
|
const peerId = `peer-replace-${Date.now()}`;
|
|
|
|
// First connection
|
|
const ws1 = new WebSocket(`${serverUrl}?peerId=${peerId}`);
|
|
await new Promise<void>((resolve) => ws1.on('open', resolve));
|
|
|
|
// Second connection with same peerId
|
|
const ws2 = new WebSocket(`${serverUrl}?peerId=${peerId}`);
|
|
await new Promise<void>((resolve) => ws2.on('open', resolve));
|
|
|
|
// First connection should be closed
|
|
await new Promise<void>((resolve) => {
|
|
ws1.on('close', (code) => {
|
|
expect(code).toBe(4002);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
ws2.close();
|
|
});
|
|
});
|
|
|
|
describe('Room Management', () => {
|
|
it('should allow peers to join rooms', async () => {
|
|
const peerId = `peer-room-${Date.now()}`;
|
|
const roomId = 'test-room-1';
|
|
const ws = new WebSocket(`${serverUrl}?peerId=${peerId}`);
|
|
|
|
await new Promise<void>((resolve) => ws.on('open', resolve));
|
|
|
|
// Join room
|
|
const joinMessage = {
|
|
type: SignalingMessageType.JOIN_ROOM,
|
|
id: generateId('msg'),
|
|
from: peerId,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { roomId },
|
|
};
|
|
|
|
const roomInfoPromise = new Promise<void>((resolve) => {
|
|
ws.on('message', (data) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === SignalingMessageType.ROOM_INFO) {
|
|
expect(message.payload.roomId).toBe(roomId);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
ws.send(JSON.stringify(joinMessage));
|
|
await roomInfoPromise;
|
|
|
|
ws.close();
|
|
});
|
|
|
|
it('should notify peers when others join the room', async () => {
|
|
const roomId = `test-room-notify-${Date.now()}`;
|
|
const peer1Id = `peer1-${Date.now()}`;
|
|
const peer2Id = `peer2-${Date.now()}`;
|
|
|
|
// Peer 1 joins
|
|
const ws1 = new WebSocket(`${serverUrl}?peerId=${peer1Id}`);
|
|
await new Promise<void>((resolve) => ws1.on('open', resolve));
|
|
|
|
ws1.send(JSON.stringify({
|
|
type: SignalingMessageType.JOIN_ROOM,
|
|
id: generateId('msg'),
|
|
from: peer1Id,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { roomId },
|
|
}));
|
|
|
|
// Wait for room info
|
|
await new Promise<void>((resolve) => {
|
|
ws1.on('message', (data) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === SignalingMessageType.ROOM_INFO) {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Set up listener for peer joined notification
|
|
const peerJoinedPromise = new Promise<void>((resolve) => {
|
|
ws1.on('message', (data) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === SignalingMessageType.PEER_JOINED) {
|
|
expect(message.payload.peerId).toBe(peer2Id);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Peer 2 joins same room
|
|
const ws2 = new WebSocket(`${serverUrl}?peerId=${peer2Id}`);
|
|
await new Promise<void>((resolve) => ws2.on('open', resolve));
|
|
|
|
ws2.send(JSON.stringify({
|
|
type: SignalingMessageType.JOIN_ROOM,
|
|
id: generateId('msg'),
|
|
from: peer2Id,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { roomId },
|
|
}));
|
|
|
|
await peerJoinedPromise;
|
|
|
|
ws1.close();
|
|
ws2.close();
|
|
});
|
|
});
|
|
|
|
describe('Message Relay', () => {
|
|
it('should relay offer messages between peers', async () => {
|
|
const roomId = `test-room-relay-${Date.now()}`;
|
|
const peer1Id = `peer1-relay-${Date.now()}`;
|
|
const peer2Id = `peer2-relay-${Date.now()}`;
|
|
|
|
// Connect both peers
|
|
const ws1 = new WebSocket(`${serverUrl}?peerId=${peer1Id}`);
|
|
const ws2 = new WebSocket(`${serverUrl}?peerId=${peer2Id}`);
|
|
|
|
await Promise.all([
|
|
new Promise<void>((resolve) => ws1.on('open', resolve)),
|
|
new Promise<void>((resolve) => ws2.on('open', resolve)),
|
|
]);
|
|
|
|
// Both join same room
|
|
for (const [ws, peerId] of [[ws1, peer1Id], [ws2, peer2Id]] as [WebSocket, string][]) {
|
|
ws.send(JSON.stringify({
|
|
type: SignalingMessageType.JOIN_ROOM,
|
|
id: generateId('msg'),
|
|
from: peerId,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { roomId },
|
|
}));
|
|
}
|
|
|
|
// Wait for room setup
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
// Set up listener for offer on peer 2
|
|
const offerPromise = new Promise<void>((resolve) => {
|
|
ws2.on('message', (data) => {
|
|
const message = JSON.parse(data.toString());
|
|
if (message.type === SignalingMessageType.OFFER) {
|
|
expect(message.from).toBe(peer1Id);
|
|
expect(message.payload.sdp).toBe('test-sdp-offer');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Peer 1 sends offer to peer 2
|
|
ws1.send(JSON.stringify({
|
|
type: SignalingMessageType.OFFER,
|
|
id: generateId('msg'),
|
|
from: peer1Id,
|
|
to: peer2Id,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { sdp: 'test-sdp-offer' },
|
|
}));
|
|
|
|
await offerPromise;
|
|
|
|
ws1.close();
|
|
ws2.close();
|
|
});
|
|
});
|
|
|
|
describe('Server Stats', () => {
|
|
it('should track connected peers and rooms', async () => {
|
|
const peerId = `peer-stats-${Date.now()}`;
|
|
const roomId = `room-stats-${Date.now()}`;
|
|
const ws = new WebSocket(`${serverUrl}?peerId=${peerId}`);
|
|
|
|
await new Promise<void>((resolve) => ws.on('open', resolve));
|
|
|
|
// Join a room
|
|
ws.send(JSON.stringify({
|
|
type: SignalingMessageType.JOIN_ROOM,
|
|
id: generateId('msg'),
|
|
from: peerId,
|
|
roomId,
|
|
timestamp: Date.now(),
|
|
payload: { roomId },
|
|
}));
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
|
|
const stats = server.getStats();
|
|
expect(stats.totalPeers).toBeGreaterThanOrEqual(1);
|
|
expect(stats.totalRooms).toBeGreaterThanOrEqual(1);
|
|
|
|
ws.close();
|
|
});
|
|
});
|
|
});
|