/** * Communication Comprehensive Test Suite * * Tests ALL communication methods between two twins: * * Hub-based (stateless): * 1. Events - fire-and-forget messaging via instance.emit/on * 2. Actions - request-response with callbacks * * WebRTC P2P (stateful): * 3. DataChannel - default channel * 4. DataChannel - named channels * 5. MediaStream - connection establishment * * Usage (two terminals or two hosts via webrtc-test.sh): * * Terminal 1 (initiator): * ``` * PHYHUB_DIRECT=true \ * DEVICE_ID= \ * ACCESS_KEY= \ * PEER_TWIN_ID= \ * ROLE=initiator \ * node dist/test/communication-comprehensive-test.js * ``` * * Terminal 2 (responder): * ``` * PHYHUB_DIRECT=true \ * DEVICE_ID= \ * ACCESS_KEY= \ * PEER_TWIN_ID= \ * ROLE=responder \ * node dist/test/communication-comprehensive-test.js * ``` */ import { PhyHubClient, Instance } from '../index'; import { PhygridDataChannel } from '../services/webrtc/types'; interface TestResult { name: string; passed: boolean; details: string; duration: number; } const MESSAGES_PER_TEST = 3; const results: TestResult[] = []; function log(msg: string): void { console.log(`[${new Date().toISOString().split('T')[1].slice(0, 8)}] ${msg}`); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function addResult(name: string, passed: boolean, details: string, startTime: number): void { const duration = Date.now() - startTime; results.push({ name, passed, details, duration }); const status = passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'; log(`[${status}] ${name}: ${details} (${duration}ms)`); } // ============================================================================= // Test 1: Events (Fire-and-Forget via Hub) // ============================================================================= async function testEvents( client: PhyHubClient, instance: Instance, peerTwinId: string, isInitiator: boolean, ): Promise { const testName = 'Events (Hub-based)'; const startTime = Date.now(); log(`\n=== Test 1: ${testName} ===`); try { let receivedCount = 0; const receivedMessages: any[] = []; // Subscribe to peer's messages await client.subscribeTwin(peerTwinId); log(`Subscribed to peer twin: ${peerTwinId}`); // Register event listener instance.on('test-event', (data: any) => { receivedCount++; receivedMessages.push(data); log(`[EVENT RECEIVED] ${JSON.stringify(data)}`); }); if (isInitiator) { // Wait for responder to be ready log('Waiting for responder to set up listeners...'); await sleep(3000); // Send test events to peer log('Sending test events...'); for (let i = 0; i < MESSAGES_PER_TEST; i++) { const eventData = { type: 'test', count: i + 1, timestamp: Date.now() }; log(`[SENDING EVENT] ${JSON.stringify(eventData)}`); // Fire-and-forget: no response expected, no promise to catch instance.to(peerTwinId).emit('test-event', eventData); await sleep(500); } // Wait for responses/echoes await sleep(5000); // Check if we received echo events from responder const passed = receivedCount >= MESSAGES_PER_TEST; addResult(testName, passed, `Sent ${MESSAGES_PER_TEST}, received echoes: ${receivedCount}`, startTime); } else { // Responder: Listen for events and echo them back log('Waiting for events from initiator...'); // Set up echo listener instance.on('test-event', (data: any) => { // Echo back to initiator (fire-and-forget: no promise to catch) const echoData = { ...data, echo: true, echoedAt: Date.now() }; log(`[ECHOING EVENT] ${JSON.stringify(echoData)}`); instance.to(peerTwinId).emit('test-event', echoData); }); // Wait for test to complete await sleep(10000); const passed = receivedCount >= MESSAGES_PER_TEST; addResult(testName, passed, `Received ${receivedCount} events from initiator`, startTime); } } catch (error: any) { addResult(testName, false, error.message, startTime); } } // ============================================================================= // Test 2: Actions (Request-Response via Hub) // ============================================================================= async function testActions( _client: PhyHubClient, instance: Instance, peerTwinId: string, isInitiator: boolean, ): Promise { const testName = 'Actions (Hub-based)'; const startTime = Date.now(); log(`\n=== Test 2: ${testName} ===`); try { let successfulActions = 0; let failedActions = 0; if (isInitiator) { // Wait for responder to set up action handlers log('Waiting for responder to set up action handlers...'); await sleep(3000); // Test: instance.to(peerTwinId).emit() with callback (action pattern) log('Testing instance.to(peerTwinId).emit() with callback (action pattern)...'); for (let i = 0; i < MESSAGES_PER_TEST; i++) { const actionType = 'test-action'; const actionPayload = { command: 'process', value: i * 10, timestamp: Date.now(), }; log(`[SENDING ACTION ${i + 1}] ${actionType}: ${JSON.stringify(actionPayload)}`); try { // emit with callback returns a Promise (action pattern) const result = await instance.to(peerTwinId).emit(actionType, actionPayload, () => {}); log(`[ACTION RESPONSE] ${JSON.stringify(result)}`); if (result?.status === 'success') { successfulActions++; } else { failedActions++; } } catch (err: any) { log(`[ACTION ERROR] ${err.message || JSON.stringify(err)}`); failedActions++; } await sleep(500); } const passed = successfulActions >= MESSAGES_PER_TEST; addResult(testName, passed, `Success: ${successfulActions}, Failed: ${failedActions}`, startTime); } else { // Responder: Handle requests using instance.on() log('Setting up request handlers via instance.on()...'); // Register request handler - the respond callback allows sending a response instance.on('test-action', (message: any, respond?: (result: any) => void) => { log(`[REQUEST RECEIVED] ${JSON.stringify(message)}`); // Send success response if (respond) { respond({ status: 'success', message: 'Request processed successfully' }); } }); // Wait for test to complete await sleep(15000); addResult(testName, true, 'Request handler registered and responded', startTime); } } catch (error: any) { addResult(testName, false, error.message, startTime); } } // ============================================================================= // Test 3: DataChannel Default (Unnamed) // ============================================================================= async function testDataChannelDefault(client: PhyHubClient, peerTwinId: string, isInitiator: boolean): Promise { const testName = 'DataChannel Default (P2P)'; const startTime = Date.now(); log(`\n=== Test 3: ${testName} ===`); try { let channel: PhygridDataChannel; let receivedCount = 0; if (isInitiator) { channel = await client.getDataChannel(peerTwinId); log(`Created default channel. Name: ${channel.getChannelName()}`); if (channel.getChannelName() !== 'default') { throw new Error(`Expected channel name 'default', got '${channel.getChannelName()}'`); } channel.onMessage((data) => { receivedCount++; log(`[DC RECEIVED] ${JSON.stringify(data)}`); }); await sleep(2000); for (let i = 0; i < MESSAGES_PER_TEST; i++) { const msg = { test: 'default', num: i, timestamp: Date.now() }; log(`[DC SENDING] ${JSON.stringify(msg)}`); channel.send(msg); await sleep(300); } await sleep(3000); addResult( testName, receivedCount >= MESSAGES_PER_TEST, `Sent ${MESSAGES_PER_TEST}, received echoes: ${receivedCount}`, startTime, ); channel.close(); } else { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timeout')), 30000); client.onDataChannel((ch) => { clearTimeout(timeout); channel = ch; log(`Received default channel. Name: ${ch.getChannelName()}`); ch.onMessage((data) => { receivedCount++; log(`[DC RECEIVED] ${JSON.stringify(data)}`); ch.send({ echo: data }); }); setTimeout(() => { addResult(testName, receivedCount >= MESSAGES_PER_TEST, `Received ${receivedCount} messages`, startTime); resolve(); }, 8000); }); }); } } catch (error: any) { addResult(testName, false, error.message, startTime); } } // ============================================================================= // Test 4: DataChannel Named Channels // ============================================================================= async function testDataChannelNamed(client: PhyHubClient, peerTwinId: string, isInitiator: boolean): Promise { const testName = 'DataChannel Named (P2P)'; const startTime = Date.now(); log(`\n=== Test 4: ${testName} ===`); const channelNames = ['control', 'data']; const receivedByChannel: Map = new Map(); try { if (isInitiator) { const channels: PhygridDataChannel[] = []; for (const name of channelNames) { const ch = await client.getDataChannel(peerTwinId, name); log(`Created channel '${name}'. Reported name: ${ch.getChannelName()}`); if (ch.getChannelName() !== name) { throw new Error(`Expected channel name '${name}', got '${ch.getChannelName()}'`); } receivedByChannel.set(name, 0); ch.onMessage(() => { receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1); }); channels.push(ch); } await sleep(3000); for (let i = 0; i < channels.length; i++) { const ch = channels[i]; for (let j = 0; j < MESSAGES_PER_TEST; j++) { ch.send({ channel: channelNames[i], num: j }); await sleep(100); } } await sleep(3000); let allPassed = true; for (const name of channelNames) { const count = receivedByChannel.get(name) || 0; if (count < MESSAGES_PER_TEST) allPassed = false; } const details = channelNames.map((n) => `${n}:${receivedByChannel.get(n)}`).join(', '); addResult(testName, allPassed, details, startTime); channels.forEach((ch) => ch.close()); } else { const promises = channelNames.map( (name) => new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000); receivedByChannel.set(name, 0); client.onDataChannel( (ch) => { clearTimeout(timeout); log(`Received channel '${name}'. Reported: ${ch.getChannelName()}`); ch.onMessage((data) => { receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1); ch.send({ echo: data }); }); setTimeout(resolve, 10000); }, { channelName: name }, ); }), ); await Promise.all(promises); const details = channelNames.map((n) => `${n}:${receivedByChannel.get(n)}`).join(', '); const allPassed = channelNames.every((n) => (receivedByChannel.get(n) || 0) >= MESSAGES_PER_TEST); addResult(testName, allPassed, details, startTime); } } catch (error: any) { addResult(testName, false, error.message, startTime); } } // ============================================================================= // Test 5: MediaStream Connection // ============================================================================= async function testMediaStreamConnection( client: PhyHubClient, peerTwinId: string, isInitiator: boolean, ): Promise { const testName = 'MediaStream (P2P)'; const startTime = Date.now(); log(`\n=== Test 5: ${testName} ===`); try { if (isInitiator) { const { stream, close } = await client.getMediaStream(peerTwinId); log(`MediaStream created. Target: ${stream.getTargetTwinId()}, Channel: ${stream.getChannelName()}`); if (stream.getChannelName() !== 'default') { throw new Error(`Expected channel name 'default', got '${stream.getChannelName()}'`); } stream.onTrack((track) => { log(`Received track: ${track.kind}`); }); await sleep(5000); addResult(testName, true, `Connected to ${stream.getTargetTwinId()}`, startTime); close(); } else { await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timeout')), 30000); client.onMediaStream((stream) => { clearTimeout(timeout); log(`MediaStream received. Channel: ${stream.getChannelName()}`); stream.onTrack((track) => { log(`Track received: ${track.kind}`); }); setTimeout(() => { addResult(testName, true, `Connected from ${stream.getTargetTwinId()}`, startTime); resolve(); }, 5000); }); }); } } catch (error: any) { addResult(testName, false, error.message, startTime); } } // ============================================================================= // Main // ============================================================================= async function main(): Promise { console.log('='.repeat(65)); console.log(' Communication Comprehensive Test Suite'); console.log(' Hub-based: Events, Actions | P2P: DataChannels, MediaStreams'); console.log('='.repeat(65)); if (process.env.PHYHUB_DIRECT !== 'true') { console.error('ERROR: Set PHYHUB_DIRECT=true'); process.exit(1); } const peerTwinId = process.env.PEER_TWIN_ID; const role = process.env.ROLE?.toLowerCase(); const testFilter = process.env.TEST; if (!peerTwinId) { console.error('ERROR: Set PEER_TWIN_ID'); process.exit(1); } if (role !== 'initiator' && role !== 'responder') { console.error('ERROR: Set ROLE to initiator or responder'); process.exit(1); } const isInitiator = role === 'initiator'; log(`Role: ${role}`); log(`Peer Twin ID: ${peerTwinId}`); if (testFilter) log(`Running only: ${testFilter}`); try { log('\nConnecting to PhyHub...'); const client = await PhyHubClient.connect(); log('Connected!'); const instance = await client.getInstance(); log(`Instance ID: ${instance.id}\n`); // Define tests type TestFunction = ( client: PhyHubClient, instanceOrPeer: any, peerOrBool: any, isInitiator?: boolean, ) => Promise; const tests: Array<{ name: string; fn: TestFunction; needsInstance: boolean }> = [ { name: 'events', fn: testEvents as TestFunction, needsInstance: true }, { name: 'actions', fn: testActions as TestFunction, needsInstance: true }, { name: 'datachannel', fn: testDataChannelDefault as TestFunction, needsInstance: false }, { name: 'datachannel-named', fn: testDataChannelNamed as TestFunction, needsInstance: false }, { name: 'mediastream', fn: testMediaStreamConnection as TestFunction, needsInstance: false }, ]; for (const test of tests) { if (!testFilter || test.name === testFilter) { if (test.needsInstance) { await test.fn(client, instance, peerTwinId, isInitiator); } else { await test.fn(client, peerTwinId, isInitiator, undefined); } await sleep(2000); } } // Print summary console.log('\n' + '='.repeat(65)); console.log('TEST SUMMARY'); console.log('='.repeat(65)); console.log('Test'.padEnd(40) + 'Status'.padEnd(10) + 'Time'); console.log('-'.repeat(65)); let passed = 0; let failed = 0; for (const r of results) { const status = r.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'; console.log(r.name.padEnd(40) + status.padEnd(15) + `${r.duration}ms`); if (r.passed) passed++; else failed++; } console.log('-'.repeat(65)); const totalTime = results.reduce((sum, r) => sum + r.duration, 0); console.log(`Total: ${passed} passed, ${failed} failed (${totalTime}ms)`); console.log('='.repeat(65)); // Give time for final cleanup await sleep(1000); process.exit(failed > 0 ? 1 : 0); } catch (error) { console.error('\n[FATAL]', error); process.exit(1); } } if (require.main === module) { main(); } export { testEvents, testActions, testDataChannelDefault, testDataChannelNamed, testMediaStreamConnection };