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.
- package/dist/adapter/AcodeSdkAdapter.d.ts +15 -0
- package/dist/adapter/AcodeSdkAdapter.js +5 -5
- package/dist/adapter/types.d.ts +9 -0
- package/dist/browser/built-in-browser-tools.js +1 -1
- package/dist/routes/system-metrics.js +1 -1
- package/dist/routes/terminal.js +19 -19
- package/dist/services/instance-features.d.ts +5 -0
- package/dist/services/instance-features.js +1 -1
- package/dist/services/process-detail.d.ts +77 -0
- package/dist/services/process-detail.js +32 -0
- package/dist/services/process-top.d.ts +53 -0
- package/dist/services/process-top.js +50 -0
- package/dist/services/session-output.d.ts +10 -0
- package/dist/services/session-output.js +3 -3
- package/package/acode/dist/built-in-file-tools.d.ts +25 -1
- package/package/acode/dist/built-in-file-tools.js +2 -2
- package/package/acode/dist/image-utils.d.ts +52 -0
- package/package/acode/dist/image-utils.js +2 -0
- package/package/acode/dist/index.d.ts +1 -0
- package/package/acode/dist/index.js +1 -1
- package/package/acode/dist/runtime.d.ts +15 -1
- package/package/acode/dist/runtime.js +26 -26
- package/package/acode/dist/types.d.ts +3 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +4 -0
- package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +8 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -25
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +6 -6
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +6 -0
- package/package/aws-client-agent-mcp/dist/memory-tools.js +2 -2
- package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
- package/package/aws-client-agent-mcp/package.json +1 -1
- package/package.json +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1066
- package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/config.test.js +0 -259
- package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -2
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -312
- package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -808
- package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +0 -110
- package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/logger.test.js +0 -27
- package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -2798
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -152
- package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -664
- package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -142
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +0 -81
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -201
- package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -157
- package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/watchdog.test.js +0 -124
- package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
- 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
|
-
});
|