aws-runtime-bridge 1.9.131 → 1.9.134

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 (63) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +15 -0
  2. package/dist/adapter/AcodeSdkAdapter.js +5 -5
  3. package/dist/adapter/types.d.ts +9 -0
  4. package/dist/browser/built-in-browser-tools.js +1 -1
  5. package/dist/routes/system-metrics.js +1 -1
  6. package/dist/routes/terminal.js +19 -19
  7. package/dist/services/instance-features.d.ts +5 -0
  8. package/dist/services/instance-features.js +1 -1
  9. package/dist/services/process-detail.d.ts +77 -0
  10. package/dist/services/process-detail.js +32 -0
  11. package/dist/services/process-top.d.ts +53 -0
  12. package/dist/services/process-top.js +50 -0
  13. package/dist/services/session-output.d.ts +10 -0
  14. package/dist/services/session-output.js +3 -3
  15. package/package/acode/dist/built-in-file-tools.d.ts +25 -1
  16. package/package/acode/dist/built-in-file-tools.js +2 -2
  17. package/package/acode/dist/image-utils.d.ts +52 -0
  18. package/package/acode/dist/image-utils.js +2 -0
  19. package/package/acode/dist/index.d.ts +1 -0
  20. package/package/acode/dist/index.js +1 -1
  21. package/package/acode/dist/runtime.d.ts +15 -1
  22. package/package/acode/dist/runtime.js +26 -26
  23. package/package/acode/dist/types.d.ts +3 -1
  24. package/package/aws-client-agent-mcp/dist/http-client.d.ts +4 -0
  25. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  26. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +8 -0
  27. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -25
  28. package/package/aws-client-agent-mcp/dist/mcp-tools.js +6 -6
  29. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +6 -0
  30. package/package/aws-client-agent-mcp/dist/memory-tools.js +2 -2
  31. package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
  32. package/package/aws-client-agent-mcp/package.json +1 -1
  33. package/package.json +1 -1
  34. package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
  35. package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1066
  36. package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
  37. package/package/aws-client-agent-mcp/dist/config.test.js +0 -259
  38. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -2
  39. package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -312
  40. package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
  41. package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -808
  42. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +0 -1
  43. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +0 -110
  44. package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
  45. package/package/aws-client-agent-mcp/dist/logger.test.js +0 -27
  46. package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
  47. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -2798
  48. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
  49. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -152
  50. package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
  51. package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -664
  52. package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
  53. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -142
  54. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +0 -1
  55. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +0 -81
  56. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
  57. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -201
  58. package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
  59. package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -157
  60. package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +0 -1
  61. package/package/aws-client-agent-mcp/dist/watchdog.test.js +0 -124
  62. package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
  63. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +0 -725
@@ -1,725 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { EventEmitter } from 'node:events';
4
- import { performance } from 'node:perf_hooks';
5
- import WebSocket from 'ws';
6
- import { WebSocketClient } from './websocket-client.js';
7
- class FakeSocket extends EventEmitter {
8
- readyState;
9
- sentPayloads = [];
10
- constructor() {
11
- super();
12
- this.readyState = WebSocket.CONNECTING;
13
- }
14
- close() {
15
- this.readyState = WebSocket.CLOSED;
16
- this.emit('close', 1000, Buffer.from('manual close'));
17
- }
18
- terminate() {
19
- this.readyState = WebSocket.CLOSED;
20
- }
21
- send(payload) {
22
- this.sentPayloads.push(payload);
23
- }
24
- ping() {
25
- setImmediate(() => this.emit('pong'));
26
- }
27
- }
28
- class SilentPingSocket extends FakeSocket {
29
- ping() {
30
- // intentionally no pong
31
- }
32
- terminate() {
33
- this.readyState = WebSocket.CLOSED;
34
- this.emit('close', 1006, Buffer.from('heartbeat timeout'));
35
- }
36
- }
37
- describe('WebSocketClient reconnect behavior', () => {
38
- it('should include runtime token in websocket URL when configured', async () => {
39
- let connectedUrl = '';
40
- const client = new WebSocketClient({
41
- serverUrl: 'ws://localhost:8080/ws/agent',
42
- heartbeatInterval: 1000,
43
- heartbeatTimeout: 3000,
44
- reconnectInterval: 20,
45
- maxReconnectAttempts: 1,
46
- runtimeAccessToken: 'aws_rt_test_token',
47
- }, (url) => {
48
- connectedUrl = url;
49
- const socket = new FakeSocket();
50
- setImmediate(() => {
51
- socket.readyState = WebSocket.OPEN;
52
- socket.emit('open');
53
- });
54
- return socket;
55
- });
56
- try {
57
- client.on('error', () => { });
58
- await client.connect('agent-test');
59
- const parsedUrl = new URL(connectedUrl);
60
- assert.equal(parsedUrl.searchParams.get('agentId'), 'agent-test');
61
- assert.equal(parsedUrl.searchParams.get('runtimeAccessToken'), 'aws_rt_test_token');
62
- }
63
- finally {
64
- client.disconnect();
65
- }
66
- });
67
- it('should force refresh runtime token once after policy rejection', async () => {
68
- const requestedBodies = [];
69
- const originalFetch = globalThis.fetch;
70
- globalThis.fetch = (async (_url, init) => {
71
- requestedBodies.push(JSON.parse(String(init?.body || '{}')));
72
- return new Response(JSON.stringify({ ok: true, runtimeAccessToken: 'fresh-token' }), {
73
- status: 200,
74
- headers: { 'Content-Type': 'application/json' },
75
- });
76
- });
77
- const connectedUrls = [];
78
- let attempt = 0;
79
- const client = new WebSocketClient({
80
- serverUrl: 'ws://localhost:8080/ws/agent',
81
- heartbeatInterval: 1000,
82
- heartbeatTimeout: 3000,
83
- reconnectInterval: 10,
84
- reconnectJitterMs: 0,
85
- maxReconnectAttempts: 2,
86
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
87
- userId: 'user-a',
88
- runtimeAccessToken: 'stale-token',
89
- }, (url) => {
90
- connectedUrls.push(url);
91
- attempt += 1;
92
- const socket = new FakeSocket();
93
- if (attempt === 1) {
94
- setImmediate(() => {
95
- socket.readyState = WebSocket.OPEN;
96
- socket.emit('open');
97
- socket.readyState = WebSocket.CLOSED;
98
- socket.emit('close', 1008, Buffer.from('policy violation'));
99
- });
100
- }
101
- else {
102
- setImmediate(() => {
103
- socket.readyState = WebSocket.OPEN;
104
- socket.emit('open');
105
- });
106
- }
107
- return socket;
108
- });
109
- try {
110
- client.on('error', () => {
111
- // suppress unhandled error event in test environment
112
- });
113
- await client.connect('agent-test');
114
- await new Promise((resolve, reject) => {
115
- const timer = setTimeout(() => reject(new Error('expected reconnect after token refresh')), 500);
116
- client.on('connected', () => {
117
- if (attempt >= 2) {
118
- clearTimeout(timer);
119
- resolve();
120
- }
121
- });
122
- });
123
- assert.equal(requestedBodies.length, 1);
124
- assert.equal(requestedBodies[0]?.forceRefresh, true);
125
- assert.equal(new URL(connectedUrls[0] || '').searchParams.get('runtimeAccessToken'), 'stale-token');
126
- assert.equal(new URL(connectedUrls[1] || '').searchParams.get('runtimeAccessToken'), 'fresh-token');
127
- }
128
- finally {
129
- client.disconnect();
130
- globalThis.fetch = originalFetch;
131
- }
132
- });
133
- it('should not hammer token refresh endpoint after repeated refresh failures', async () => {
134
- let refreshRequests = 0;
135
- const originalFetch = globalThis.fetch;
136
- globalThis.fetch = (async () => {
137
- refreshRequests += 1;
138
- return new Response(JSON.stringify({ error: 'refresh failed' }), {
139
- status: 500,
140
- headers: { 'Content-Type': 'application/json' },
141
- });
142
- });
143
- let attempt = 0;
144
- const client = new WebSocketClient({
145
- serverUrl: 'ws://localhost:8080/ws/agent',
146
- heartbeatInterval: 1000,
147
- heartbeatTimeout: 3000,
148
- reconnectInterval: 10,
149
- reconnectJitterMs: 0,
150
- maxReconnectAttempts: 3,
151
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
152
- userId: 'user-a',
153
- runtimeAccessToken: 'stale-token',
154
- }, () => {
155
- attempt += 1;
156
- const socket = new FakeSocket();
157
- setImmediate(() => {
158
- socket.readyState = WebSocket.OPEN;
159
- socket.emit('open');
160
- socket.readyState = WebSocket.CLOSED;
161
- socket.emit('close', 1008, Buffer.from('policy violation'));
162
- });
163
- return socket;
164
- });
165
- try {
166
- client.on('error', () => {
167
- // suppress unhandled error event in test environment
168
- });
169
- await client.connect('agent-test');
170
- // 每次失败重连触发一次刷新;连续失败达到上限后不再高频重连/刷新。
171
- // maxReconnectAttempts=3:3 次连接尝试 → 3 次 refresh → reconnectFailed 停止。
172
- await new Promise((resolve, reject) => {
173
- const timer = setTimeout(() => reject(new Error('expected reconnectFailed after refresh failures')), 1000);
174
- client.on('reconnectFailed', () => {
175
- clearTimeout(timer);
176
- resolve();
177
- });
178
- });
179
- // 停止后再观察一段窗口,确认没有继续 hammer 刷新接口
180
- await new Promise((resolve) => setTimeout(resolve, 150));
181
- assert.equal(attempt, 3, 'should stop reconnecting after refresh failures reach the limit');
182
- assert.equal(refreshRequests, 3, 'one refresh per reconnect attempt, no extra hammering');
183
- }
184
- finally {
185
- client.disconnect();
186
- globalThis.fetch = originalFetch;
187
- }
188
- });
189
- it('should keep slow-retrying after unchanged token refresh instead of giving up', async () => {
190
- let refreshRequests = 0;
191
- let tokenRejectedCount = 0;
192
- const originalFetch = globalThis.fetch;
193
- // bridge 始终返回与当前相同的 token(unchanged):换 token 无法解决 1008
194
- globalThis.fetch = (async () => {
195
- refreshRequests += 1;
196
- return new Response(JSON.stringify({ runtimeAccessToken: 'stale-token' }), {
197
- status: 200,
198
- headers: { 'Content-Type': 'application/json' },
199
- });
200
- });
201
- let attempt = 0;
202
- const client = new WebSocketClient({
203
- serverUrl: 'ws://localhost:8080/ws/agent',
204
- heartbeatInterval: 1000,
205
- heartbeatTimeout: 3000,
206
- reconnectInterval: 5,
207
- reconnectJitterMs: 0,
208
- maxReconnectAttempts: -1,
209
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
210
- userId: 'user-a',
211
- runtimeAccessToken: 'stale-token',
212
- }, () => {
213
- attempt += 1;
214
- const socket = new FakeSocket();
215
- setImmediate(() => {
216
- socket.readyState = WebSocket.OPEN;
217
- socket.emit('open');
218
- socket.readyState = WebSocket.CLOSED;
219
- socket.emit('close', 1008, Buffer.from('policy violation'));
220
- });
221
- return socket;
222
- });
223
- try {
224
- client.on('error', () => {
225
- // suppress unhandled error event in test environment
226
- });
227
- client.on('tokenRejected', () => {
228
- tokenRejectedCount += 1;
229
- });
230
- await client.connect('agent-test');
231
- // 旧行为:unchanged 后永久放弃重连(Agent 永久离线)。
232
- // 新行为:慢速重试持续进行——重连与刷新以退避节奏继续,等待新 token 签发自愈。
233
- await new Promise((resolve, reject) => {
234
- const timer = setTimeout(() => reject(new Error('expected continued slow retries after unchanged refresh')), 2000);
235
- const check = setInterval(() => {
236
- if (attempt >= 4) {
237
- clearInterval(check);
238
- clearTimeout(timer);
239
- resolve();
240
- }
241
- }, 5);
242
- });
243
- assert.ok(attempt >= 4, 'reconnect attempts should continue after unchanged refresh');
244
- assert.ok(tokenRejectedCount >= 3, 'each 1008 rejection with unchanged token should be observable');
245
- assert.ok(refreshRequests >= 3, 'refresh should keep being attempted at backoff cadence');
246
- }
247
- finally {
248
- client.disconnect();
249
- // close 事件处理链是 async(token 刷新 → 慢速重连排程),与 disconnect 存在竞态:
250
- // 等在飞链路落定后再清一次,避免残留的长退避 timer 拖住测试进程
251
- await new Promise((resolve) => setTimeout(resolve, 50));
252
- client.disconnect();
253
- globalThis.fetch = originalFetch;
254
- }
255
- });
256
- it('should abort runtime token refresh after configured timeout', async () => {
257
- const originalFetch = globalThis.fetch;
258
- let signalSeen = false;
259
- globalThis.fetch = (async (_url, init) => {
260
- signalSeen = init?.signal instanceof AbortSignal;
261
- return await new Promise((_resolve, reject) => {
262
- init?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
263
- });
264
- });
265
- let attempt = 0;
266
- const client = new WebSocketClient({
267
- serverUrl: 'ws://localhost:8080/ws/agent',
268
- heartbeatInterval: 1000,
269
- heartbeatTimeout: 3000,
270
- reconnectInterval: 10,
271
- reconnectJitterMs: 0,
272
- maxReconnectAttempts: 1,
273
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
274
- userId: 'user-a',
275
- runtimeAccessToken: 'stale-token',
276
- mcpHttpRequestTimeoutMs: 10,
277
- }, () => {
278
- attempt += 1;
279
- const socket = new FakeSocket();
280
- setImmediate(() => {
281
- socket.readyState = WebSocket.OPEN;
282
- socket.emit('open');
283
- socket.readyState = WebSocket.CLOSED;
284
- socket.emit('close', 1008, Buffer.from('policy violation'));
285
- });
286
- return socket;
287
- });
288
- try {
289
- client.on('error', () => {
290
- // suppress unhandled error event in test environment
291
- });
292
- await client.connect('agent-test');
293
- await new Promise((resolve, reject) => {
294
- const timer = setTimeout(() => reject(new Error('expected reconnect after refresh timeout')), 500);
295
- client.on('disconnected', () => {
296
- if (attempt >= 2) {
297
- clearTimeout(timer);
298
- resolve();
299
- }
300
- });
301
- });
302
- assert.equal(signalSeen, true);
303
- }
304
- finally {
305
- client.disconnect();
306
- globalThis.fetch = originalFetch;
307
- }
308
- });
309
- it('should refresh again when a different runtime token is later rejected', async () => {
310
- const requestedBodies = [];
311
- const refreshedTokens = ['fresh-token-1', 'fresh-token-2'];
312
- const originalFetch = globalThis.fetch;
313
- globalThis.fetch = (async (_url, init) => {
314
- requestedBodies.push(JSON.parse(String(init?.body || '{}')));
315
- return new Response(JSON.stringify({ ok: true, runtimeAccessToken: refreshedTokens.shift() }), {
316
- status: 200,
317
- headers: { 'Content-Type': 'application/json' },
318
- });
319
- });
320
- let attempt = 0;
321
- const createdSockets = [];
322
- const config = {
323
- serverUrl: 'ws://localhost:8080/ws/agent',
324
- heartbeatInterval: 1000,
325
- heartbeatTimeout: 3000,
326
- reconnectInterval: 10,
327
- reconnectJitterMs: 0,
328
- maxReconnectAttempts: 5,
329
- runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
330
- userId: 'user-a',
331
- runtimeAccessToken: 'stale-token-1',
332
- };
333
- const client = new WebSocketClient(config, () => {
334
- attempt += 1;
335
- const socket = new FakeSocket();
336
- createdSockets.push(socket);
337
- setImmediate(() => {
338
- socket.readyState = WebSocket.OPEN;
339
- socket.emit('open');
340
- if (attempt === 1) {
341
- socket.readyState = WebSocket.CLOSED;
342
- socket.emit('close', 1008, Buffer.from('policy violation'));
343
- }
344
- });
345
- return socket;
346
- });
347
- try {
348
- client.on('error', () => {
349
- // suppress unhandled error event in test environment
350
- });
351
- await client.connect('agent-test');
352
- await new Promise((resolve, reject) => {
353
- const timer = setTimeout(() => reject(new Error('expected second token refresh')), 500);
354
- const interval = setInterval(() => {
355
- if (requestedBodies.length >= 2 && config.runtimeAccessToken === 'fresh-token-2') {
356
- clearInterval(interval);
357
- clearTimeout(timer);
358
- resolve();
359
- }
360
- }, 5);
361
- client.on('connected', () => {
362
- if (attempt === 2) {
363
- config.runtimeAccessToken = 'stale-token-2';
364
- const activeSocket = createdSockets[createdSockets.length - 1];
365
- if (activeSocket) {
366
- activeSocket.readyState = WebSocket.CLOSED;
367
- activeSocket.emit('close', 1008, Buffer.from('policy violation'));
368
- }
369
- }
370
- });
371
- });
372
- assert.equal(requestedBodies.length, 2);
373
- assert.equal(config.runtimeAccessToken, 'fresh-token-2');
374
- }
375
- finally {
376
- client.disconnect();
377
- globalThis.fetch = originalFetch;
378
- }
379
- });
380
- it('should keep retrying at interval after scheduler restart until connected', async () => {
381
- const createdSockets = [];
382
- let attempt = 0;
383
- const client = new WebSocketClient({
384
- serverUrl: 'ws://localhost:8080/ws/agent',
385
- heartbeatInterval: 1000,
386
- heartbeatTimeout: 3000,
387
- reconnectInterval: 20,
388
- reconnectJitterMs: 0,
389
- maxReconnectAttempts: -1
390
- }, (_url) => {
391
- const socket = new FakeSocket();
392
- createdSockets.push(socket);
393
- attempt += 1;
394
- if (attempt === 1 || attempt >= 4) {
395
- setImmediate(() => {
396
- socket.readyState = WebSocket.OPEN;
397
- socket.emit('open');
398
- });
399
- }
400
- else {
401
- setImmediate(() => {
402
- socket.emit('error', new Error('ECONNREFUSED'));
403
- socket.emit('close', 1006, Buffer.from('connect fail'));
404
- });
405
- }
406
- return socket;
407
- });
408
- try {
409
- client.on('error', () => {
410
- // suppress unhandled error event in test environment
411
- });
412
- await client.connect('agent-test');
413
- const firstSocket = createdSockets[0];
414
- assert.ok(firstSocket, 'first socket should exist');
415
- firstSocket.readyState = WebSocket.CLOSED;
416
- firstSocket.emit('close', 1006, Buffer.from('server restart'));
417
- await new Promise((resolve, reject) => {
418
- const timer = setTimeout(() => {
419
- reject(new Error('expected reconnect to eventually succeed'));
420
- }, 500);
421
- client.on('connected', () => {
422
- if (attempt >= 4) {
423
- clearTimeout(timer);
424
- resolve();
425
- }
426
- });
427
- });
428
- assert.equal(attempt, 4, 'should attempt initial + retries until success');
429
- assert.equal(client.isConnected(), true);
430
- }
431
- finally {
432
- client.disconnect();
433
- }
434
- });
435
- it('should wait reconnect interval before first reconnect attempt', async () => {
436
- const reconnectInterval = 80;
437
- const attemptTimes = [];
438
- let attempt = 0;
439
- const client = new WebSocketClient({
440
- serverUrl: 'ws://localhost:8080/ws/agent',
441
- heartbeatInterval: 1000,
442
- heartbeatTimeout: 3000,
443
- reconnectInterval,
444
- reconnectJitterMs: 0,
445
- maxReconnectAttempts: 2,
446
- }, (_url) => {
447
- const socket = new FakeSocket();
448
- attempt += 1;
449
- attemptTimes.push(performance.now());
450
- if (attempt === 1) {
451
- setImmediate(() => {
452
- socket.readyState = WebSocket.OPEN;
453
- socket.emit('open');
454
- });
455
- }
456
- else {
457
- setImmediate(() => {
458
- socket.readyState = WebSocket.OPEN;
459
- socket.emit('open');
460
- });
461
- }
462
- return socket;
463
- });
464
- try {
465
- client.on('error', () => {
466
- // suppress unhandled error event in test environment
467
- });
468
- await client.connect('agent-test');
469
- const firstSocketTime = attemptTimes[0];
470
- const firstSocket = client.ws;
471
- assert.ok(firstSocket, 'first socket should exist');
472
- firstSocket.readyState = WebSocket.CLOSED;
473
- firstSocket.emit('close', 1006, Buffer.from('server restart'));
474
- await new Promise((resolve, reject) => {
475
- const timer = setTimeout(() => {
476
- reject(new Error('expected reconnect attempt'));
477
- }, 500);
478
- const check = setInterval(() => {
479
- if (attempt >= 2) {
480
- clearInterval(check);
481
- clearTimeout(timer);
482
- resolve();
483
- }
484
- }, 5);
485
- });
486
- assert.ok(attemptTimes[1] - firstSocketTime >= reconnectInterval - 15, `first reconnect should wait about ${reconnectInterval}ms, actual=${attemptTimes[1] - firstSocketTime}ms`);
487
- }
488
- finally {
489
- client.disconnect();
490
- }
491
- });
492
- it('should terminate and reconnect when heartbeat response times out', async () => {
493
- const createdSockets = [];
494
- let attempt = 0;
495
- const client = new WebSocketClient({
496
- serverUrl: 'ws://localhost:8080/ws/agent',
497
- heartbeatInterval: 10,
498
- heartbeatTimeout: 20,
499
- reconnectInterval: 10,
500
- reconnectJitterMs: 0,
501
- maxReconnectAttempts: 2,
502
- }, (_url) => {
503
- attempt += 1;
504
- const socket = attempt === 1 ? new SilentPingSocket() : new FakeSocket();
505
- createdSockets.push(socket);
506
- setImmediate(() => {
507
- socket.readyState = WebSocket.OPEN;
508
- socket.emit('open');
509
- });
510
- return socket;
511
- });
512
- try {
513
- client.on('error', () => {
514
- // suppress unhandled error event in test environment
515
- });
516
- await client.connect('agent-test');
517
- await new Promise((resolve, reject) => {
518
- const timer = setTimeout(() => {
519
- reject(new Error('expected reconnect after heartbeat timeout'));
520
- }, 500);
521
- client.on('connected', () => {
522
- if (attempt >= 2) {
523
- clearTimeout(timer);
524
- resolve();
525
- }
526
- });
527
- });
528
- assert.equal(createdSockets[0]?.readyState, WebSocket.CLOSED);
529
- assert.equal(attempt, 2);
530
- assert.equal(client.isConnected(), true);
531
- }
532
- finally {
533
- client.disconnect();
534
- }
535
- });
536
- it('should acknowledge reliable direct messages when received', async () => {
537
- const client = new WebSocketClient({
538
- serverUrl: 'ws://localhost:8080/ws/agent',
539
- heartbeatInterval: 1000,
540
- heartbeatTimeout: 3000,
541
- reconnectInterval: 20,
542
- maxReconnectAttempts: 1,
543
- }, (_url) => {
544
- const socket = new FakeSocket();
545
- setImmediate(() => {
546
- socket.readyState = WebSocket.OPEN;
547
- socket.emit('open');
548
- });
549
- return socket;
550
- });
551
- try {
552
- client.on('error', () => {
553
- // suppress unhandled error event in test environment
554
- });
555
- await client.connect('agent-test');
556
- const socket = client.ws;
557
- assert.ok(socket, 'socket should exist');
558
- socket.emit('message', Buffer.from(JSON.stringify({
559
- type: 'DM_MSG',
560
- data: {
561
- msgId: 'dm-1',
562
- _msgId: 'delivery-1',
563
- _requireAck: true,
564
- senderId: 'agent-a',
565
- senderName: 'Agent A',
566
- receiverId: 'agent-test',
567
- content: 'hello',
568
- requireReply: false,
569
- timestamp: '2026-05-06T10:00:00.000Z',
570
- },
571
- timestamp: '2026-05-06T10:00:00.000Z',
572
- })));
573
- const ackPayload = socket.sentPayloads
574
- .map(payload => JSON.parse(payload))
575
- .find(payload => payload.type === 'MESSAGE_ACK');
576
- assert.deepEqual(ackPayload, {
577
- type: 'MESSAGE_ACK',
578
- data: { msgId: 'delivery-1' },
579
- timestamp: ackPayload?.timestamp,
580
- });
581
- }
582
- finally {
583
- client.disconnect();
584
- }
585
- });
586
- it('should skip duplicate reliable messages but still ACK them', async () => {
587
- const directMessageCalls = [];
588
- const client = new WebSocketClient({
589
- serverUrl: 'ws://localhost:8080/ws/agent',
590
- heartbeatInterval: 1000,
591
- heartbeatTimeout: 3000,
592
- reconnectInterval: 20,
593
- maxReconnectAttempts: 1,
594
- }, (_url) => {
595
- const socket = new FakeSocket();
596
- setImmediate(() => {
597
- socket.readyState = WebSocket.OPEN;
598
- socket.emit('open');
599
- });
600
- return socket;
601
- });
602
- try {
603
- client.on('error', () => { });
604
- client.on('directMessage', (data) => {
605
- directMessageCalls.push(data);
606
- });
607
- await client.connect('agent-test');
608
- const socket = client.ws;
609
- assert.ok(socket, 'socket should exist');
610
- const buildPayload = () => JSON.stringify({
611
- type: 'DM_MSG',
612
- data: {
613
- msgId: 'dm-dup-1',
614
- _msgId: 'delivery-dup-1',
615
- _requireAck: true,
616
- senderId: 'agent-a',
617
- senderName: 'Agent A',
618
- receiverId: 'agent-test',
619
- content: 'hello',
620
- requireReply: false,
621
- timestamp: '2026-05-06T10:00:00.000Z',
622
- },
623
- timestamp: '2026-05-06T10:00:00.000Z',
624
- });
625
- // 模拟服务端 ACK 超时后重发同一 messageId
626
- socket.emit('message', Buffer.from(buildPayload()));
627
- socket.emit('message', Buffer.from(buildPayload()));
628
- const ackPayloads = socket.sentPayloads
629
- .map(payload => JSON.parse(payload))
630
- .filter(payload => payload.type === 'MESSAGE_ACK' && payload.data?.msgId === 'delivery-dup-1');
631
- // 重复投递的消息仍需回 ACK,避免服务端继续重试堆积
632
- assert.equal(ackPayloads.length, 2, 'should ACK both first and duplicate delivery');
633
- // 业务侧只能收到一次
634
- assert.equal(directMessageCalls.length, 1, 'should deliver duplicate message to handler only once');
635
- }
636
- finally {
637
- client.disconnect();
638
- }
639
- });
640
- it('should ignore terminal status messages without warning', async () => {
641
- const originalWarn = console.warn;
642
- const warnMessages = [];
643
- console.warn = (...args) => {
644
- warnMessages.push(args.map(String).join(' '));
645
- };
646
- const client = new WebSocketClient({
647
- serverUrl: 'ws://localhost:8080/ws/agent',
648
- heartbeatInterval: 1000,
649
- heartbeatTimeout: 3000,
650
- reconnectInterval: 20,
651
- maxReconnectAttempts: 1,
652
- }, (_url) => {
653
- const socket = new FakeSocket();
654
- setImmediate(() => {
655
- socket.readyState = WebSocket.OPEN;
656
- socket.emit('open');
657
- });
658
- return socket;
659
- });
660
- try {
661
- client.on('error', () => {
662
- // suppress unhandled error event in test environment
663
- });
664
- await client.connect('agent-test');
665
- const socket = client.ws;
666
- assert.ok(socket, 'socket should exist');
667
- socket.emit('message', Buffer.from(JSON.stringify({
668
- type: 'TERMINAL_STATUS',
669
- data: {
670
- agentId: 'agent-test',
671
- status: 'running',
672
- sessionId: 'session-1',
673
- },
674
- timestamp: '2026-05-06T10:00:00.000Z',
675
- })));
676
- assert.equal(warnMessages.some(message => message.includes('未知消息类型')), false);
677
- }
678
- finally {
679
- client.disconnect();
680
- console.warn = originalWarn;
681
- }
682
- });
683
- it('should ignore MCP node log broadcasts without warning', async () => {
684
- const originalWarn = console.warn;
685
- const warnMessages = [];
686
- console.warn = (...args) => {
687
- warnMessages.push(args.map(String).join(' '));
688
- };
689
- const client = new WebSocketClient({
690
- serverUrl: 'ws://localhost:8080/ws/agent',
691
- heartbeatInterval: 1000,
692
- heartbeatTimeout: 3000,
693
- reconnectInterval: 20,
694
- maxReconnectAttempts: 1,
695
- }, (_url) => {
696
- const socket = new FakeSocket();
697
- setImmediate(() => {
698
- socket.readyState = WebSocket.OPEN;
699
- socket.emit('open');
700
- });
701
- return socket;
702
- });
703
- try {
704
- client.on('error', () => { });
705
- await client.connect('agent-test');
706
- const socket = client.ws;
707
- assert.ok(socket, 'socket should exist');
708
- socket.emit('message', Buffer.from(JSON.stringify({
709
- type: 'MCP_NODE_LOG',
710
- data: {
711
- agentId: 'agent-test',
712
- level: 'info',
713
- message: 'AI request started',
714
- timestamp: 1_780_000_000_000,
715
- },
716
- timestamp: '2026-05-06T10:00:00.000Z',
717
- })));
718
- assert.equal(warnMessages.some(message => message.includes('MCP_NODE_LOG')), false);
719
- }
720
- finally {
721
- client.disconnect();
722
- console.warn = originalWarn;
723
- }
724
- });
725
- });