@phystack/hub-client 4.5.19-dev → 4.5.20-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/index.d.ts +22 -28
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +252 -378
  4. package/dist/index.js.map +1 -1
  5. package/dist/peripheral-twin.d.ts +34 -0
  6. package/dist/peripheral-twin.d.ts.map +1 -0
  7. package/dist/peripheral-twin.js +234 -0
  8. package/dist/peripheral-twin.js.map +1 -0
  9. package/dist/services/phyhub-connection.service.d.ts +1 -0
  10. package/dist/services/phyhub-connection.service.d.ts.map +1 -1
  11. package/dist/services/phyhub-connection.service.js +17 -0
  12. package/dist/services/phyhub-connection.service.js.map +1 -1
  13. package/dist/services/phyhub-direct-connection.service.d.ts +21 -0
  14. package/dist/services/phyhub-direct-connection.service.d.ts.map +1 -0
  15. package/dist/services/phyhub-direct-connection.service.js +101 -0
  16. package/dist/services/phyhub-direct-connection.service.js.map +1 -0
  17. package/dist/services/webrtc/data-channel-handler.d.ts +45 -0
  18. package/dist/services/webrtc/data-channel-handler.d.ts.map +1 -0
  19. package/dist/services/webrtc/data-channel-handler.js +260 -0
  20. package/dist/services/webrtc/data-channel-handler.js.map +1 -0
  21. package/dist/services/webrtc/index.d.ts +8 -0
  22. package/dist/services/webrtc/index.d.ts.map +1 -0
  23. package/dist/services/webrtc/index.js +18 -0
  24. package/dist/services/webrtc/index.js.map +1 -0
  25. package/dist/services/webrtc/media-stream-handler.d.ts +57 -0
  26. package/dist/services/webrtc/media-stream-handler.d.ts.map +1 -0
  27. package/dist/services/webrtc/media-stream-handler.js +383 -0
  28. package/dist/services/webrtc/media-stream-handler.js.map +1 -0
  29. package/dist/services/webrtc/peer-connection-manager.d.ts +40 -0
  30. package/dist/services/webrtc/peer-connection-manager.d.ts.map +1 -0
  31. package/dist/services/webrtc/peer-connection-manager.js +336 -0
  32. package/dist/services/webrtc/peer-connection-manager.js.map +1 -0
  33. package/dist/services/webrtc/types.d.ts +134 -0
  34. package/dist/services/webrtc/types.d.ts.map +1 -0
  35. package/dist/services/webrtc/types.js +12 -0
  36. package/dist/services/webrtc/types.js.map +1 -0
  37. package/dist/services/webrtc/webrtc-globals.d.ts +4 -0
  38. package/dist/services/webrtc/webrtc-globals.d.ts.map +1 -0
  39. package/dist/services/webrtc/webrtc-globals.js +72 -0
  40. package/dist/services/webrtc/webrtc-globals.js.map +1 -0
  41. package/dist/services/webrtc/webrtc-manager.d.ts +35 -0
  42. package/dist/services/webrtc/webrtc-manager.d.ts.map +1 -0
  43. package/dist/services/webrtc/webrtc-manager.js +274 -0
  44. package/dist/services/webrtc/webrtc-manager.js.map +1 -0
  45. package/dist/test/communication-comprehensive-test.d.ts +8 -0
  46. package/dist/test/communication-comprehensive-test.d.ts.map +1 -0
  47. package/dist/test/communication-comprehensive-test.js +356 -0
  48. package/dist/test/communication-comprehensive-test.js.map +1 -0
  49. package/dist/test/webrtc-channel-names-test.d.ts +2 -0
  50. package/dist/test/webrtc-channel-names-test.d.ts.map +1 -0
  51. package/dist/test/webrtc-channel-names-test.js +177 -0
  52. package/dist/test/webrtc-channel-names-test.js.map +1 -0
  53. package/dist/test/webrtc-comprehensive-test.d.ts +2 -0
  54. package/dist/test/webrtc-comprehensive-test.d.ts.map +1 -0
  55. package/dist/test/webrtc-comprehensive-test.js +328 -0
  56. package/dist/test/webrtc-comprehensive-test.js.map +1 -0
  57. package/dist/test/webrtc-reconnect-test.d.ts +4 -0
  58. package/dist/test/webrtc-reconnect-test.d.ts.map +1 -0
  59. package/dist/test/webrtc-reconnect-test.js +244 -0
  60. package/dist/test/webrtc-reconnect-test.js.map +1 -0
  61. package/dist/test/webrtc-test-harness.d.ts +4 -0
  62. package/dist/test/webrtc-test-harness.d.ts.map +1 -0
  63. package/dist/test/webrtc-test-harness.js +169 -0
  64. package/dist/test/webrtc-test-harness.js.map +1 -0
  65. package/dist/twin-messaging.d.ts +20 -0
  66. package/dist/twin-messaging.d.ts.map +1 -0
  67. package/dist/twin-messaging.js +94 -0
  68. package/dist/twin-messaging.js.map +1 -0
  69. package/dist/twin-registry.d.ts +9 -0
  70. package/dist/twin-registry.d.ts.map +1 -0
  71. package/dist/twin-registry.js +26 -0
  72. package/dist/twin-registry.js.map +1 -0
  73. package/dist/types/index.d.ts +4 -0
  74. package/dist/types/index.d.ts.map +1 -0
  75. package/dist/types/index.js +20 -0
  76. package/dist/types/index.js.map +1 -0
  77. package/dist/types/twin.types.d.ts +62 -14
  78. package/dist/types/twin.types.d.ts.map +1 -1
  79. package/dist/types/twin.types.js +8 -1
  80. package/dist/types/twin.types.js.map +1 -1
  81. package/docs/webrtc-howto.md +398 -0
  82. package/docs/webrtc-test.md +330 -0
  83. package/package.json +3 -3
  84. package/scripts/webrtc-test.sh +401 -0
  85. package/src/index.ts +378 -568
  86. package/src/peripheral-twin.ts +337 -0
  87. package/src/services/phyhub-connection.service.ts +24 -0
  88. package/src/services/phyhub-direct-connection.service.ts +159 -0
  89. package/src/services/webrtc/data-channel-handler.ts +362 -0
  90. package/src/services/webrtc/index.ts +36 -0
  91. package/src/services/webrtc/media-stream-handler.ts +536 -0
  92. package/src/services/webrtc/peer-connection-manager.ts +467 -0
  93. package/src/services/webrtc/types.ts +273 -0
  94. package/src/services/webrtc/webrtc-globals.ts +108 -0
  95. package/src/services/webrtc/webrtc-manager.ts +490 -0
  96. package/src/test/communication-comprehensive-test.ts +533 -0
  97. package/src/test/webrtc-channel-names-test.ts +266 -0
  98. package/src/test/webrtc-comprehensive-test.ts +494 -0
  99. package/src/test/webrtc-reconnect-test.ts +345 -0
  100. package/src/test/webrtc-test-harness.ts +254 -0
  101. package/src/twin-messaging.ts +184 -0
  102. package/src/twin-registry.ts +39 -0
  103. package/src/types/index.ts +3 -0
  104. package/src/types/twin.types.ts +80 -14
  105. package/dist/services/webrtc/datachannel.d.ts +0 -10
  106. package/dist/services/webrtc/datachannel.d.ts.map +0 -1
  107. package/dist/services/webrtc/datachannel.js +0 -290
  108. package/dist/services/webrtc/datachannel.js.map +0 -1
  109. package/dist/services/webrtc/mediastream.d.ts +0 -10
  110. package/dist/services/webrtc/mediastream.d.ts.map +0 -1
  111. package/dist/services/webrtc/mediastream.js +0 -396
  112. package/dist/services/webrtc/mediastream.js.map +0 -1
  113. package/dist/services/webrtc/peer-connection-ice.d.ts +0 -32
  114. package/dist/services/webrtc/peer-connection-ice.d.ts.map +0 -1
  115. package/dist/services/webrtc/peer-connection-ice.js +0 -483
  116. package/dist/services/webrtc/peer-connection-ice.js.map +0 -1
  117. package/src/services/webrtc/datachannel.ts +0 -421
  118. package/src/services/webrtc/mediastream.ts +0 -602
  119. package/src/services/webrtc/peer-connection-ice.ts +0 -689
@@ -0,0 +1,494 @@
1
+ /**
2
+ * WebRTC Comprehensive Test Suite
3
+ *
4
+ * Tests the following scenarios:
5
+ * 1. DataChannel - default (unnamed) channel
6
+ * 2. DataChannel - named channels (multiple to same peer)
7
+ * 3. DataChannel - reconnection with handler persistence
8
+ * 4. MediaStream - connection establishment
9
+ * 5. MediaStream - named channels
10
+ *
11
+ * Usage (two terminals):
12
+ *
13
+ * Terminal 1 (initiator):
14
+ * ```
15
+ * PHYHUB_DIRECT=true \
16
+ * DEVICE_ID=<device-a-id> \
17
+ * ACCESS_KEY=<device-a-key> \
18
+ * PEER_TWIN_ID=<device-b-twin-id> \
19
+ * ROLE=initiator \
20
+ * npx ts-node src/test/webrtc-comprehensive-test.ts
21
+ * ```
22
+ *
23
+ * Terminal 2 (responder):
24
+ * ```
25
+ * PHYHUB_DIRECT=true \
26
+ * DEVICE_ID=<device-b-id> \
27
+ * ACCESS_KEY=<device-b-key> \
28
+ * PEER_TWIN_ID=<device-a-twin-id> \
29
+ * ROLE=responder \
30
+ * npx ts-node src/test/webrtc-comprehensive-test.ts
31
+ * ```
32
+ */
33
+
34
+ import { PhyHubClient } from '../index';
35
+ import { PhygridDataChannel } from '../services/webrtc/types';
36
+
37
+ interface TestResult {
38
+ name: string;
39
+ passed: boolean;
40
+ details: string;
41
+ }
42
+
43
+ const MESSAGES_PER_TEST = 3;
44
+ const results: TestResult[] = [];
45
+
46
+ function log(msg: string): void {
47
+ console.log(`[${new Date().toISOString().split('T')[1].slice(0, 8)}] ${msg}`);
48
+ }
49
+
50
+ function sleep(ms: number): Promise<void> {
51
+ return new Promise((resolve) => setTimeout(resolve, ms));
52
+ }
53
+
54
+ function addResult(name: string, passed: boolean, details: string): void {
55
+ results.push({ name, passed, details });
56
+ const status = passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
57
+ log(`[${status}] ${name}: ${details}`);
58
+ }
59
+
60
+ // =============================================================================
61
+ // Test 1: DataChannel Default (Unnamed)
62
+ // =============================================================================
63
+ async function testDataChannelDefault(
64
+ client: PhyHubClient,
65
+ peerTwinId: string,
66
+ isInitiator: boolean
67
+ ): Promise<void> {
68
+ log('\n=== Test 1: DataChannel Default ===');
69
+
70
+ try {
71
+ let channel: PhygridDataChannel;
72
+ let receivedCount = 0;
73
+
74
+ if (isInitiator) {
75
+ channel = await client.getDataChannel(peerTwinId);
76
+ log(`Created default channel. Name: ${channel.getChannelName()}`);
77
+
78
+ if (channel.getChannelName() !== 'default') {
79
+ throw new Error(`Expected channel name 'default', got '${channel.getChannelName()}'`);
80
+ }
81
+
82
+ channel.onMessage(() => receivedCount++);
83
+ await sleep(2000);
84
+
85
+ for (let i = 0; i < MESSAGES_PER_TEST; i++) {
86
+ channel.send({ test: 'default', num: i });
87
+ await sleep(200);
88
+ }
89
+ await sleep(2000);
90
+
91
+ addResult('DataChannel Default', receivedCount >= MESSAGES_PER_TEST,
92
+ `Sent ${MESSAGES_PER_TEST}, received ${receivedCount}`);
93
+ } else {
94
+ await new Promise<void>((resolve, reject) => {
95
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
96
+
97
+ client.onDataChannel(peerTwinId, (ch) => {
98
+ clearTimeout(timeout);
99
+ channel = ch;
100
+ log(`Received default channel. Name: ${ch.getChannelName()}`);
101
+
102
+ ch.onMessage((data) => {
103
+ receivedCount++;
104
+ ch.send({ echo: data });
105
+ });
106
+
107
+ // Wait for messages then resolve
108
+ setTimeout(() => {
109
+ addResult('DataChannel Default', receivedCount >= MESSAGES_PER_TEST,
110
+ `Received ${receivedCount} messages`);
111
+ resolve();
112
+ }, 8000);
113
+ });
114
+ });
115
+ }
116
+ } catch (error: any) {
117
+ addResult('DataChannel Default', false, error.message);
118
+ }
119
+ }
120
+
121
+ // =============================================================================
122
+ // Test 2: DataChannel Named Channels
123
+ // =============================================================================
124
+ async function testDataChannelNamed(
125
+ client: PhyHubClient,
126
+ peerTwinId: string,
127
+ isInitiator: boolean
128
+ ): Promise<void> {
129
+ log('\n=== Test 2: DataChannel Named Channels ===');
130
+
131
+ const channelNames = ['control', 'data', 'telemetry'];
132
+ const receivedByChannel: Map<string, number> = new Map();
133
+
134
+ try {
135
+ if (isInitiator) {
136
+ const channels: PhygridDataChannel[] = [];
137
+
138
+ // Create all named channels
139
+ for (const name of channelNames) {
140
+ const ch = await client.getDataChannel(peerTwinId, name);
141
+ log(`Created channel '${name}'. Reported name: ${ch.getChannelName()}`);
142
+
143
+ if (ch.getChannelName() !== name) {
144
+ throw new Error(`Expected channel name '${name}', got '${ch.getChannelName()}'`);
145
+ }
146
+
147
+ receivedByChannel.set(name, 0);
148
+ ch.onMessage(() => {
149
+ receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
150
+ });
151
+ channels.push(ch);
152
+ }
153
+
154
+ await sleep(3000);
155
+
156
+ // Send messages on each channel
157
+ for (let i = 0; i < channels.length; i++) {
158
+ const ch = channels[i];
159
+ for (let j = 0; j < MESSAGES_PER_TEST; j++) {
160
+ ch.send({ channel: channelNames[i], num: j });
161
+ await sleep(100);
162
+ }
163
+ }
164
+
165
+ await sleep(3000);
166
+
167
+ // Check results
168
+ let allPassed = true;
169
+ for (const name of channelNames) {
170
+ const count = receivedByChannel.get(name) || 0;
171
+ if (count < MESSAGES_PER_TEST) allPassed = false;
172
+ }
173
+
174
+ const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
175
+ addResult('DataChannel Named', allPassed, details);
176
+
177
+ channels.forEach(ch => ch.close());
178
+ } else {
179
+ // Responder: accept all named channels
180
+ const promises = channelNames.map(name => new Promise<void>((resolve, reject) => {
181
+ const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000);
182
+ receivedByChannel.set(name, 0);
183
+
184
+ client.onDataChannel(peerTwinId, (ch) => {
185
+ clearTimeout(timeout);
186
+ log(`Received channel '${name}'. Reported: ${ch.getChannelName()}`);
187
+
188
+ ch.onMessage((data) => {
189
+ receivedByChannel.set(name, (receivedByChannel.get(name) || 0) + 1);
190
+ ch.send({ echo: data });
191
+ });
192
+
193
+ setTimeout(resolve, 10000);
194
+ }, name);
195
+ }));
196
+
197
+ await Promise.all(promises);
198
+
199
+ const details = channelNames.map(n => `${n}:${receivedByChannel.get(n)}`).join(', ');
200
+ const allPassed = channelNames.every(n => (receivedByChannel.get(n) || 0) >= MESSAGES_PER_TEST);
201
+ addResult('DataChannel Named', allPassed, details);
202
+ }
203
+ } catch (error: any) {
204
+ addResult('DataChannel Named', false, error.message);
205
+ }
206
+ }
207
+
208
+ // =============================================================================
209
+ // Test 3: DataChannel Reconnection
210
+ // =============================================================================
211
+ async function testDataChannelReconnection(
212
+ client: PhyHubClient,
213
+ peerTwinId: string,
214
+ isInitiator: boolean
215
+ ): Promise<void> {
216
+ log('\n=== Test 3: DataChannel Reconnection ===');
217
+
218
+ try {
219
+ const manager = await client.getWebRTCManager({ verbose: true });
220
+ let reconnectDetected = false;
221
+ let messagesAfterReconnect = 0;
222
+ let channel: PhygridDataChannel;
223
+
224
+ manager.on('reconnecting', () => {
225
+ log('[EVENT] Reconnecting...');
226
+ });
227
+ manager.on('reconnected', () => {
228
+ log('[EVENT] Reconnected!');
229
+ reconnectDetected = true;
230
+ });
231
+
232
+ if (isInitiator) {
233
+ channel = await client.getDataChannel(peerTwinId, 'reconnect-test');
234
+ log('Channel created for reconnect test');
235
+
236
+ // Handler registered ONCE - should persist across reconnect
237
+ channel.onMessage(() => messagesAfterReconnect++);
238
+
239
+ // Send initial messages
240
+ for (let i = 0; i < 3; i++) {
241
+ channel.send({ phase: 'before', num: i });
242
+ await sleep(200);
243
+ }
244
+
245
+ log('Waiting 5s before simulating disconnect...');
246
+ await sleep(5000);
247
+
248
+ // Simulate disconnect by requesting it from responder
249
+ channel.send({ command: 'simulate-disconnect' });
250
+
251
+ log('Waiting 15s for reconnection...');
252
+ await sleep(15000);
253
+
254
+ // Send messages after reconnection
255
+ for (let i = 0; i < MESSAGES_PER_TEST; i++) {
256
+ channel.send({ phase: 'after', num: i });
257
+ await sleep(200);
258
+ }
259
+
260
+ await sleep(3000);
261
+
262
+ const passed = messagesAfterReconnect >= MESSAGES_PER_TEST;
263
+ addResult('DataChannel Reconnection', passed,
264
+ `Reconnect detected: ${reconnectDetected}, messages after: ${messagesAfterReconnect}`);
265
+ } else {
266
+ await new Promise<void>((resolve, reject) => {
267
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 60000);
268
+
269
+ client.onDataChannel(peerTwinId, (ch) => {
270
+ clearTimeout(timeout);
271
+ channel = ch;
272
+ log('Reconnect test channel received');
273
+
274
+ // Handler registered ONCE
275
+ ch.onMessage((data) => {
276
+ messagesAfterReconnect++;
277
+ ch.send({ echo: data });
278
+
279
+ if (data.command === 'simulate-disconnect') {
280
+ log('Simulating disconnect...');
281
+ // Note: In a real test, we would close the underlying connection
282
+ // For now, we just acknowledge the command
283
+ }
284
+ });
285
+
286
+ setTimeout(() => {
287
+ addResult('DataChannel Reconnection', messagesAfterReconnect > 0,
288
+ `Received ${messagesAfterReconnect} total messages`);
289
+ resolve();
290
+ }, 30000);
291
+ }, 'reconnect-test');
292
+ });
293
+ }
294
+ } catch (error: any) {
295
+ addResult('DataChannel Reconnection', false, error.message);
296
+ }
297
+ }
298
+
299
+ // =============================================================================
300
+ // Test 4: MediaStream Connection
301
+ // =============================================================================
302
+ async function testMediaStreamConnection(
303
+ client: PhyHubClient,
304
+ peerTwinId: string,
305
+ isInitiator: boolean
306
+ ): Promise<void> {
307
+ log('\n=== Test 4: MediaStream Connection ===');
308
+
309
+ try {
310
+ if (isInitiator) {
311
+ const { stream, close } = await client.getMediaStream(peerTwinId);
312
+ log(`MediaStream created. Target: ${stream.getTargetTwinId()}, Channel: ${stream.getChannelName()}`);
313
+
314
+ if (stream.getChannelName() !== 'default') {
315
+ throw new Error(`Expected channel name 'default', got '${stream.getChannelName()}'`);
316
+ }
317
+
318
+ // Track listener
319
+ stream.onTrack((track) => {
320
+ log(`Received track: ${track.kind}`);
321
+ });
322
+
323
+ await sleep(5000);
324
+
325
+ addResult('MediaStream Connection', true,
326
+ `Connected to ${stream.getTargetTwinId()}`);
327
+
328
+ close();
329
+ } else {
330
+ await new Promise<void>((resolve, reject) => {
331
+ const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
332
+
333
+ client.onMediaStream(peerTwinId, (stream) => {
334
+ clearTimeout(timeout);
335
+ log(`MediaStream received. Channel: ${stream.getChannelName()}`);
336
+
337
+ stream.onTrack((track) => {
338
+ log(`Track received: ${track.kind}`);
339
+ });
340
+
341
+ setTimeout(() => {
342
+ addResult('MediaStream Connection', true,
343
+ `Connected from ${stream.getTargetTwinId()}`);
344
+ resolve();
345
+ }, 5000);
346
+ });
347
+ });
348
+ }
349
+ } catch (error: any) {
350
+ addResult('MediaStream Connection', false, error.message);
351
+ }
352
+ }
353
+
354
+ // =============================================================================
355
+ // Test 5: MediaStream Named Channels
356
+ // =============================================================================
357
+ async function testMediaStreamNamed(
358
+ client: PhyHubClient,
359
+ peerTwinId: string,
360
+ isInitiator: boolean
361
+ ): Promise<void> {
362
+ log('\n=== Test 5: MediaStream Named Channels ===');
363
+
364
+ const channelNames = ['video', 'audio'];
365
+
366
+ try {
367
+ if (isInitiator) {
368
+ const streams: Array<{ stream: any; close: () => void }> = [];
369
+
370
+ for (const name of channelNames) {
371
+ const result = await client.getMediaStream(peerTwinId, { channelName: name });
372
+ log(`Created MediaStream '${name}'. Reported: ${result.stream.getChannelName()}`);
373
+
374
+ if (result.stream.getChannelName() !== name) {
375
+ throw new Error(`Expected '${name}', got '${result.stream.getChannelName()}'`);
376
+ }
377
+
378
+ streams.push(result);
379
+ }
380
+
381
+ await sleep(5000);
382
+
383
+ addResult('MediaStream Named', true,
384
+ `Created ${channelNames.length} named streams`);
385
+
386
+ streams.forEach(s => s.close());
387
+ } else {
388
+ const receivedChannels: string[] = [];
389
+
390
+ const promises = channelNames.map(name => new Promise<void>((resolve, reject) => {
391
+ const timeout = setTimeout(() => reject(new Error(`Timeout for ${name}`)), 30000);
392
+
393
+ client.onMediaStream(peerTwinId, (stream) => {
394
+ clearTimeout(timeout);
395
+ log(`Received MediaStream '${name}'. Reported: ${stream.getChannelName()}`);
396
+ receivedChannels.push(stream.getChannelName());
397
+ setTimeout(resolve, 3000);
398
+ }, { channelName: name });
399
+ }));
400
+
401
+ await Promise.all(promises);
402
+
403
+ const passed = receivedChannels.length === channelNames.length;
404
+ addResult('MediaStream Named', passed,
405
+ `Received channels: ${receivedChannels.join(', ')}`);
406
+ }
407
+ } catch (error: any) {
408
+ addResult('MediaStream Named', false, error.message);
409
+ }
410
+ }
411
+
412
+ // =============================================================================
413
+ // Main
414
+ // =============================================================================
415
+ async function main(): Promise<void> {
416
+ console.log('='.repeat(60));
417
+ console.log('WebRTC Comprehensive Test Suite');
418
+ console.log('='.repeat(60));
419
+
420
+ if (process.env.PHYHUB_DIRECT !== 'true') {
421
+ console.error('ERROR: Set PHYHUB_DIRECT=true');
422
+ process.exit(1);
423
+ }
424
+
425
+ const peerTwinId = process.env.PEER_TWIN_ID;
426
+ const role = process.env.ROLE?.toLowerCase();
427
+ const testFilter = process.env.TEST; // Optional: run specific test
428
+
429
+ if (!peerTwinId) {
430
+ console.error('ERROR: Set PEER_TWIN_ID');
431
+ process.exit(1);
432
+ }
433
+
434
+ if (role !== 'initiator' && role !== 'responder') {
435
+ console.error('ERROR: Set ROLE to initiator or responder');
436
+ process.exit(1);
437
+ }
438
+
439
+ const isInitiator = role === 'initiator';
440
+ log(`Role: ${role}`);
441
+ log(`Peer Twin ID: ${peerTwinId}`);
442
+ if (testFilter) log(`Running only: ${testFilter}`);
443
+
444
+ try {
445
+ log('\nConnecting to PhyHub...');
446
+ const client = await PhyHubClient.connect();
447
+ log('Connected!\n');
448
+
449
+ // Run tests based on filter
450
+ const tests = [
451
+ { name: 'default', fn: testDataChannelDefault },
452
+ { name: 'named', fn: testDataChannelNamed },
453
+ { name: 'reconnect', fn: testDataChannelReconnection },
454
+ { name: 'media', fn: testMediaStreamConnection },
455
+ { name: 'media-named', fn: testMediaStreamNamed },
456
+ ];
457
+
458
+ for (const test of tests) {
459
+ if (!testFilter || test.name === testFilter) {
460
+ await test.fn(client, peerTwinId, isInitiator);
461
+ await sleep(2000);
462
+ }
463
+ }
464
+
465
+ // Print summary
466
+ console.log('\n' + '='.repeat(60));
467
+ console.log('TEST SUMMARY');
468
+ console.log('='.repeat(60));
469
+ console.log('Test'.padEnd(30) + 'Status');
470
+ console.log('-'.repeat(45));
471
+
472
+ let passed = 0;
473
+ let failed = 0;
474
+ for (const r of results) {
475
+ const status = r.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
476
+ console.log(r.name.padEnd(30) + status);
477
+ if (r.passed) passed++;
478
+ else failed++;
479
+ }
480
+
481
+ console.log('-'.repeat(45));
482
+ console.log(`Total: ${passed} passed, ${failed} failed`);
483
+ console.log('='.repeat(60));
484
+
485
+ process.exit(failed > 0 ? 1 : 0);
486
+ } catch (error) {
487
+ console.error('\n[FATAL]', error);
488
+ process.exit(1);
489
+ }
490
+ }
491
+
492
+ if (require.main === module) {
493
+ main();
494
+ }