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,808 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
|
|
3
|
-
import { HttpClient } from './http-client.js';
|
|
4
|
-
describe('HttpClient', { concurrency: false }, () => {
|
|
5
|
-
const originalFetch = global.fetch;
|
|
6
|
-
let fetchCalls = [];
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
fetchCalls = [];
|
|
9
|
-
global.fetch = async (url, options) => {
|
|
10
|
-
fetchCalls.push([url.toString(), options]);
|
|
11
|
-
return {
|
|
12
|
-
ok: true,
|
|
13
|
-
status: 200,
|
|
14
|
-
statusText: 'OK',
|
|
15
|
-
json: async () => ({ result: 'success' }),
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
});
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
global.fetch = originalFetch;
|
|
21
|
-
mock.restoreAll();
|
|
22
|
-
});
|
|
23
|
-
describe('constructor and URL resolution', () => {
|
|
24
|
-
it('uses explicit mcpHttpUrl when provided', () => {
|
|
25
|
-
const config = {
|
|
26
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
27
|
-
heartbeatInterval: 15000,
|
|
28
|
-
reconnectInterval: 200,
|
|
29
|
-
maxReconnectAttempts: -1,
|
|
30
|
-
mcpHttpUrl: 'http://custom:3000/mcp/call',
|
|
31
|
-
};
|
|
32
|
-
const client = new HttpClient(config);
|
|
33
|
-
assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
|
|
34
|
-
});
|
|
35
|
-
it('appends /mcp/call to mcpHttpUrl when not present', () => {
|
|
36
|
-
const config = {
|
|
37
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
38
|
-
heartbeatInterval: 15000,
|
|
39
|
-
reconnectInterval: 200,
|
|
40
|
-
maxReconnectAttempts: -1,
|
|
41
|
-
mcpHttpUrl: 'http://custom:3000',
|
|
42
|
-
};
|
|
43
|
-
const client = new HttpClient(config);
|
|
44
|
-
assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
|
|
45
|
-
});
|
|
46
|
-
it('appends /mcp/call to mcpHttpUrl with trailing slash', () => {
|
|
47
|
-
const config = {
|
|
48
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
49
|
-
heartbeatInterval: 15000,
|
|
50
|
-
reconnectInterval: 200,
|
|
51
|
-
maxReconnectAttempts: -1,
|
|
52
|
-
mcpHttpUrl: 'http://custom:3000/',
|
|
53
|
-
};
|
|
54
|
-
const client = new HttpClient(config);
|
|
55
|
-
assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/mcp/call');
|
|
56
|
-
});
|
|
57
|
-
it('derives HTTP URL from WebSocket URL (ws://)', () => {
|
|
58
|
-
const config = {
|
|
59
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
60
|
-
heartbeatInterval: 15000,
|
|
61
|
-
reconnectInterval: 200,
|
|
62
|
-
maxReconnectAttempts: -1,
|
|
63
|
-
};
|
|
64
|
-
const client = new HttpClient(config);
|
|
65
|
-
assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
|
|
66
|
-
});
|
|
67
|
-
it('derives HTTP URL from WebSocket URL (wss://)', () => {
|
|
68
|
-
const config = {
|
|
69
|
-
serverUrl: 'wss://secure.example.com/ws/agent',
|
|
70
|
-
heartbeatInterval: 15000,
|
|
71
|
-
reconnectInterval: 200,
|
|
72
|
-
maxReconnectAttempts: -1,
|
|
73
|
-
};
|
|
74
|
-
const client = new HttpClient(config);
|
|
75
|
-
assert.equal(client.getMcpHttpUrl(), 'https://secure.example.com/mcp/call');
|
|
76
|
-
});
|
|
77
|
-
it('handles empty mcpHttpUrl by deriving from serverUrl', () => {
|
|
78
|
-
const config = {
|
|
79
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
80
|
-
heartbeatInterval: 15000,
|
|
81
|
-
reconnectInterval: 200,
|
|
82
|
-
maxReconnectAttempts: -1,
|
|
83
|
-
mcpHttpUrl: '',
|
|
84
|
-
};
|
|
85
|
-
const client = new HttpClient(config);
|
|
86
|
-
assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
|
|
87
|
-
});
|
|
88
|
-
it('handles whitespace-only mcpHttpUrl', () => {
|
|
89
|
-
const config = {
|
|
90
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
91
|
-
heartbeatInterval: 15000,
|
|
92
|
-
reconnectInterval: 200,
|
|
93
|
-
maxReconnectAttempts: -1,
|
|
94
|
-
mcpHttpUrl: ' ',
|
|
95
|
-
};
|
|
96
|
-
const client = new HttpClient(config);
|
|
97
|
-
assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
|
|
98
|
-
});
|
|
99
|
-
it('repairs duplicated ports from configured mcpHttpUrl', () => {
|
|
100
|
-
const config = {
|
|
101
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
102
|
-
heartbeatInterval: 15000,
|
|
103
|
-
reconnectInterval: 200,
|
|
104
|
-
maxReconnectAttempts: -1,
|
|
105
|
-
mcpHttpUrl: 'http://127.0.0.1:8080:8080/mcp/call',
|
|
106
|
-
};
|
|
107
|
-
const client = new HttpClient(config);
|
|
108
|
-
assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
|
|
109
|
-
});
|
|
110
|
-
it('repairs duplicated ports from serverUrl', () => {
|
|
111
|
-
const config = {
|
|
112
|
-
serverUrl: 'ws://127.0.0.1:8080:8080/ws/agent',
|
|
113
|
-
heartbeatInterval: 15000,
|
|
114
|
-
reconnectInterval: 200,
|
|
115
|
-
maxReconnectAttempts: -1,
|
|
116
|
-
};
|
|
117
|
-
const client = new HttpClient(config);
|
|
118
|
-
assert.equal(client.getMcpHttpUrl(), 'http://127.0.0.1:8080/mcp/call');
|
|
119
|
-
});
|
|
120
|
-
it('preserves configured MCP HTTP subpaths', () => {
|
|
121
|
-
const config = {
|
|
122
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
123
|
-
heartbeatInterval: 15000,
|
|
124
|
-
reconnectInterval: 200,
|
|
125
|
-
maxReconnectAttempts: -1,
|
|
126
|
-
mcpHttpUrl: 'http://custom:3000/api',
|
|
127
|
-
};
|
|
128
|
-
const client = new HttpClient(config);
|
|
129
|
-
assert.equal(client.getMcpHttpUrl(), 'http://custom:3000/api/mcp/call');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe('callTool', { concurrency: false }, () => {
|
|
133
|
-
it('makes POST request with runtime token headers', async () => {
|
|
134
|
-
const config = {
|
|
135
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
136
|
-
heartbeatInterval: 15000,
|
|
137
|
-
reconnectInterval: 200,
|
|
138
|
-
maxReconnectAttempts: -1,
|
|
139
|
-
runtimeAccessToken: 'runtime-token',
|
|
140
|
-
};
|
|
141
|
-
const client = new HttpClient(config);
|
|
142
|
-
await client.callTool('test-tool', { arg1: 'value1' });
|
|
143
|
-
assert.equal(fetchCalls.length, 1);
|
|
144
|
-
const [url, options] = fetchCalls[0];
|
|
145
|
-
assert.equal(url, 'http://127.0.0.1:8080/mcp/call');
|
|
146
|
-
assert.equal(options.method, 'POST');
|
|
147
|
-
assert.equal(options.headers['Content-Type'], 'application/json');
|
|
148
|
-
assert.equal(options.headers['X-Runtime-Token'], 'runtime-token');
|
|
149
|
-
assert.equal(options.headers.Authorization, 'Bearer runtime-token');
|
|
150
|
-
assert.equal(options.body, JSON.stringify({ tool: 'test-tool', arguments: { arg1: 'value1' } }));
|
|
151
|
-
});
|
|
152
|
-
it('makes POST request without runtime token when not configured', async () => {
|
|
153
|
-
const config = {
|
|
154
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
155
|
-
heartbeatInterval: 15000,
|
|
156
|
-
reconnectInterval: 200,
|
|
157
|
-
maxReconnectAttempts: -1,
|
|
158
|
-
};
|
|
159
|
-
const client = new HttpClient(config);
|
|
160
|
-
await client.callTool('test-tool', {});
|
|
161
|
-
const [, options] = fetchCalls[0];
|
|
162
|
-
assert.equal(options.headers['X-Runtime-Token'], undefined);
|
|
163
|
-
assert.equal(options.headers.Authorization, undefined);
|
|
164
|
-
});
|
|
165
|
-
it('sets an AbortSignal for poll_message long-poll requests', async () => {
|
|
166
|
-
const config = {
|
|
167
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
168
|
-
heartbeatInterval: 15000,
|
|
169
|
-
reconnectInterval: 200,
|
|
170
|
-
maxReconnectAttempts: -1,
|
|
171
|
-
runtimeAccessToken: 'runtime-token',
|
|
172
|
-
};
|
|
173
|
-
const client = new HttpClient(config);
|
|
174
|
-
await client.callTool('poll_message', { agentId: 'agent-1' });
|
|
175
|
-
const [, options] = fetchCalls[0];
|
|
176
|
-
assert.ok(options.signal instanceof AbortSignal);
|
|
177
|
-
});
|
|
178
|
-
it('keeps an AbortSignal for non-blocking MCP tools', async () => {
|
|
179
|
-
const config = {
|
|
180
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
181
|
-
heartbeatInterval: 15000,
|
|
182
|
-
reconnectInterval: 200,
|
|
183
|
-
maxReconnectAttempts: -1,
|
|
184
|
-
runtimeAccessToken: 'runtime-token',
|
|
185
|
-
};
|
|
186
|
-
const client = new HttpClient(config);
|
|
187
|
-
await client.callTool('get_message', { agentId: 'agent-1' });
|
|
188
|
-
const [, options] = fetchCalls[0];
|
|
189
|
-
assert.ok(options.signal instanceof AbortSignal);
|
|
190
|
-
});
|
|
191
|
-
it('returns parsed result on success', async () => {
|
|
192
|
-
const config = {
|
|
193
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
194
|
-
heartbeatInterval: 15000,
|
|
195
|
-
reconnectInterval: 200,
|
|
196
|
-
maxReconnectAttempts: -1,
|
|
197
|
-
};
|
|
198
|
-
const client = new HttpClient(config);
|
|
199
|
-
global.fetch = async () => ({
|
|
200
|
-
ok: true,
|
|
201
|
-
json: async () => ({ data: 'test-data', count: 42 }),
|
|
202
|
-
});
|
|
203
|
-
const result = await client.callTool('test-tool', {});
|
|
204
|
-
assert.deepEqual(result, { data: 'test-data', count: 42 });
|
|
205
|
-
});
|
|
206
|
-
it('throws error when response is not ok', async () => {
|
|
207
|
-
const config = {
|
|
208
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
209
|
-
heartbeatInterval: 15000,
|
|
210
|
-
reconnectInterval: 200,
|
|
211
|
-
maxReconnectAttempts: -1,
|
|
212
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
213
|
-
};
|
|
214
|
-
const client = new HttpClient(config);
|
|
215
|
-
global.fetch = async () => ({
|
|
216
|
-
ok: false,
|
|
217
|
-
status: 500,
|
|
218
|
-
statusText: 'Internal Server Error',
|
|
219
|
-
});
|
|
220
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 500 Internal Server Error/);
|
|
221
|
-
});
|
|
222
|
-
it('includes response body when response is not ok', async () => {
|
|
223
|
-
const config = {
|
|
224
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
225
|
-
heartbeatInterval: 15000,
|
|
226
|
-
reconnectInterval: 200,
|
|
227
|
-
maxReconnectAttempts: -1,
|
|
228
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
229
|
-
};
|
|
230
|
-
const client = new HttpClient(config);
|
|
231
|
-
global.fetch = async () => ({
|
|
232
|
-
ok: false,
|
|
233
|
-
status: 503,
|
|
234
|
-
statusText: 'Service Unavailable',
|
|
235
|
-
text: async () => '{"error":"Async request timed out"}',
|
|
236
|
-
});
|
|
237
|
-
await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message call failed: 503 Service Unavailable - {"error":"Async request timed out"}/);
|
|
238
|
-
});
|
|
239
|
-
it('throws a clear tool error when result contains error field', async () => {
|
|
240
|
-
const config = {
|
|
241
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
242
|
-
heartbeatInterval: 15000,
|
|
243
|
-
reconnectInterval: 200,
|
|
244
|
-
maxReconnectAttempts: -1,
|
|
245
|
-
};
|
|
246
|
-
const client = new HttpClient(config);
|
|
247
|
-
global.fetch = async () => ({
|
|
248
|
-
ok: true,
|
|
249
|
-
json: async () => ({ error: 'Tool execution failed' }),
|
|
250
|
-
});
|
|
251
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool failed: Tool execution failed/);
|
|
252
|
-
});
|
|
253
|
-
it('handles 401 unauthorized response', async () => {
|
|
254
|
-
const config = {
|
|
255
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
256
|
-
heartbeatInterval: 15000,
|
|
257
|
-
reconnectInterval: 200,
|
|
258
|
-
maxReconnectAttempts: -1,
|
|
259
|
-
};
|
|
260
|
-
const client = new HttpClient(config);
|
|
261
|
-
global.fetch = async () => ({
|
|
262
|
-
ok: false,
|
|
263
|
-
status: 401,
|
|
264
|
-
statusText: 'Unauthorized',
|
|
265
|
-
});
|
|
266
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 401 Unauthorized/);
|
|
267
|
-
});
|
|
268
|
-
it('force-refreshes runtime token and retries once on 401', async () => {
|
|
269
|
-
const config = {
|
|
270
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
271
|
-
heartbeatInterval: 15000,
|
|
272
|
-
reconnectInterval: 200,
|
|
273
|
-
maxReconnectAttempts: -1,
|
|
274
|
-
userId: 'user-1',
|
|
275
|
-
runtimeAccessToken: 'stale-token',
|
|
276
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
277
|
-
};
|
|
278
|
-
const client = new HttpClient(config);
|
|
279
|
-
global.fetch = async (url, options) => {
|
|
280
|
-
fetchCalls.push([url.toString(), options]);
|
|
281
|
-
if (url.toString() === 'http://127.0.0.1:8080/mcp/call' && fetchCalls.length === 1) {
|
|
282
|
-
return {
|
|
283
|
-
ok: false,
|
|
284
|
-
status: 401,
|
|
285
|
-
statusText: 'Unauthorized',
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
289
|
-
return {
|
|
290
|
-
ok: true,
|
|
291
|
-
status: 200,
|
|
292
|
-
statusText: 'OK',
|
|
293
|
-
json: async () => ({ runtimeAccessToken: 'fresh-token' }),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
ok: true,
|
|
298
|
-
status: 200,
|
|
299
|
-
statusText: 'OK',
|
|
300
|
-
json: async () => ({ data: 'retried' }),
|
|
301
|
-
};
|
|
302
|
-
};
|
|
303
|
-
const result = await client.callTool('my_task', { agentId: 'agent-1' });
|
|
304
|
-
assert.deepEqual(result, { data: 'retried' });
|
|
305
|
-
assert.equal(fetchCalls.length, 3);
|
|
306
|
-
assert.equal(fetchCalls[0][1].headers['X-Runtime-Token'], 'stale-token');
|
|
307
|
-
assert.equal(fetchCalls[1][0], 'http://127.0.0.1:18081/runtime/binding/refresh-token');
|
|
308
|
-
assert.equal(fetchCalls[1][1].body, JSON.stringify({
|
|
309
|
-
agentId: 'agent-1',
|
|
310
|
-
currentToken: 'stale-token',
|
|
311
|
-
}));
|
|
312
|
-
assert.equal(fetchCalls[2][1].headers['X-Runtime-Token'], 'fresh-token');
|
|
313
|
-
assert.equal(config.runtimeAccessToken, 'fresh-token');
|
|
314
|
-
});
|
|
315
|
-
it('shares one bridge token refresh across concurrent 401 responses', async () => {
|
|
316
|
-
const config = {
|
|
317
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
318
|
-
heartbeatInterval: 15000,
|
|
319
|
-
reconnectInterval: 200,
|
|
320
|
-
maxReconnectAttempts: -1,
|
|
321
|
-
userId: 'user-1',
|
|
322
|
-
runtimeAccessToken: 'stale-token',
|
|
323
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
324
|
-
};
|
|
325
|
-
const client = new HttpClient(config);
|
|
326
|
-
const staleTokenCalls = new Set();
|
|
327
|
-
global.fetch = async (url, options) => {
|
|
328
|
-
fetchCalls.push([url.toString(), options]);
|
|
329
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
330
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
331
|
-
return {
|
|
332
|
-
ok: true,
|
|
333
|
-
status: 200,
|
|
334
|
-
statusText: 'OK',
|
|
335
|
-
json: async () => ({ runtimeAccessToken: 'fresh-token' }),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const body = JSON.parse(String(options?.body || '{}'));
|
|
339
|
-
const token = options.headers['X-Runtime-Token'];
|
|
340
|
-
if (token === 'stale-token') {
|
|
341
|
-
staleTokenCalls.add(body.tool || '');
|
|
342
|
-
return {
|
|
343
|
-
ok: false,
|
|
344
|
-
status: 401,
|
|
345
|
-
statusText: 'Unauthorized',
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
ok: true,
|
|
350
|
-
status: 200,
|
|
351
|
-
statusText: 'OK',
|
|
352
|
-
json: async () => ({ tool: body.tool, token }),
|
|
353
|
-
};
|
|
354
|
-
};
|
|
355
|
-
const [first, second] = await Promise.all([
|
|
356
|
-
client.callTool('get_memory_stats', { agentId: 'agent-1' }),
|
|
357
|
-
client.callTool('discover_group_rooms', { agentId: 'agent-1' }),
|
|
358
|
-
]);
|
|
359
|
-
assert.deepEqual(first, { tool: 'get_memory_stats', token: 'fresh-token' });
|
|
360
|
-
assert.deepEqual(second, { tool: 'discover_group_rooms', token: 'fresh-token' });
|
|
361
|
-
assert.deepEqual(staleTokenCalls, new Set(['get_memory_stats', 'discover_group_rooms']));
|
|
362
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
|
|
363
|
-
assert.equal(config.runtimeAccessToken, 'fresh-token');
|
|
364
|
-
});
|
|
365
|
-
it('throws when no runtimeAccessToken and no bridge for startup', async () => {
|
|
366
|
-
const config = {
|
|
367
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
368
|
-
heartbeatInterval: 15000,
|
|
369
|
-
reconnectInterval: 200,
|
|
370
|
-
maxReconnectAttempts: -1,
|
|
371
|
-
userId: 'user-1',
|
|
372
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
373
|
-
};
|
|
374
|
-
const client = new HttpClient(config);
|
|
375
|
-
global.fetch = async () => ({
|
|
376
|
-
ok: true,
|
|
377
|
-
status: 200,
|
|
378
|
-
statusText: 'OK',
|
|
379
|
-
json: async () => ({ result: 'success' }),
|
|
380
|
-
});
|
|
381
|
-
await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /runtimeAccessToken is required; MCP nodes must claim a session token from launch binding/);
|
|
382
|
-
});
|
|
383
|
-
it('throws original 401 when bridge refresh fails', async () => {
|
|
384
|
-
const config = {
|
|
385
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
386
|
-
heartbeatInterval: 15000,
|
|
387
|
-
reconnectInterval: 200,
|
|
388
|
-
maxReconnectAttempts: -1,
|
|
389
|
-
userId: 'user-1',
|
|
390
|
-
runtimeAccessToken: 'stale-token',
|
|
391
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
392
|
-
};
|
|
393
|
-
const client = new HttpClient(config);
|
|
394
|
-
global.fetch = async (url, options) => {
|
|
395
|
-
fetchCalls.push([url.toString(), options]);
|
|
396
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
397
|
-
return {
|
|
398
|
-
ok: false,
|
|
399
|
-
status: 503,
|
|
400
|
-
statusText: 'Service Unavailable',
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
ok: false,
|
|
405
|
-
status: 401,
|
|
406
|
-
statusText: 'Unauthorized',
|
|
407
|
-
};
|
|
408
|
-
};
|
|
409
|
-
await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /MCP tool get_memory_stats call failed: 401 Unauthorized/);
|
|
410
|
-
// Can retry again after failure
|
|
411
|
-
global.fetch = async (url, options) => {
|
|
412
|
-
fetchCalls.push([url.toString(), options]);
|
|
413
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
414
|
-
return {
|
|
415
|
-
ok: true,
|
|
416
|
-
status: 200,
|
|
417
|
-
statusText: 'OK',
|
|
418
|
-
json: async () => ({ runtimeAccessToken: 'recovered-token' }),
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
const token = options.headers['X-Runtime-Token'];
|
|
422
|
-
if (token === 'recovered-token') {
|
|
423
|
-
return {
|
|
424
|
-
ok: true,
|
|
425
|
-
status: 200,
|
|
426
|
-
statusText: 'OK',
|
|
427
|
-
json: async () => ({ token }),
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
ok: false,
|
|
432
|
-
status: 401,
|
|
433
|
-
statusText: 'Unauthorized',
|
|
434
|
-
};
|
|
435
|
-
};
|
|
436
|
-
const result = await client.callTool('get_memory_stats', { agentId: 'agent-1' });
|
|
437
|
-
assert.deepEqual(result, { token: 'recovered-token' });
|
|
438
|
-
assert.equal(config.runtimeAccessToken, 'recovered-token');
|
|
439
|
-
});
|
|
440
|
-
it('returns 401 when bridge refresh has no new token for running agent', async () => {
|
|
441
|
-
const config = {
|
|
442
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
443
|
-
heartbeatInterval: 15000,
|
|
444
|
-
reconnectInterval: 200,
|
|
445
|
-
maxReconnectAttempts: -1,
|
|
446
|
-
runtimeAccessToken: 'stale-token',
|
|
447
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
448
|
-
};
|
|
449
|
-
const client = new HttpClient(config);
|
|
450
|
-
global.fetch = async (url, options) => {
|
|
451
|
-
fetchCalls.push([url.toString(), options]);
|
|
452
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
453
|
-
// Bridge says no new token available (409 conflict)
|
|
454
|
-
return {
|
|
455
|
-
ok: false,
|
|
456
|
-
status: 409,
|
|
457
|
-
statusText: 'Conflict',
|
|
458
|
-
json: async () => ({ error: 'token_unchanged' }),
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
return {
|
|
462
|
-
ok: false,
|
|
463
|
-
status: 401,
|
|
464
|
-
statusText: 'Unauthorized',
|
|
465
|
-
};
|
|
466
|
-
};
|
|
467
|
-
await assert.rejects(() => client.callTool('get_memory_stats', { agentId: 'agent-1' }), /MCP tool get_memory_stats call failed: 401 Unauthorized/);
|
|
468
|
-
// 1 initial MCP call + 1 refresh attempt + 0 retries (refresh failed)
|
|
469
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
|
|
470
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
|
|
471
|
-
assert.equal(config.runtimeAccessToken, 'stale-token');
|
|
472
|
-
});
|
|
473
|
-
it('returns 404 from bridge refresh when agent has no cached token', async () => {
|
|
474
|
-
const config = {
|
|
475
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
476
|
-
heartbeatInterval: 15000,
|
|
477
|
-
reconnectInterval: 200,
|
|
478
|
-
maxReconnectAttempts: -1,
|
|
479
|
-
userId: 'user-1',
|
|
480
|
-
runtimeAccessToken: 'stale-token',
|
|
481
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
482
|
-
};
|
|
483
|
-
const client = new HttpClient(config);
|
|
484
|
-
global.fetch = async (url, options) => {
|
|
485
|
-
fetchCalls.push([url.toString(), options]);
|
|
486
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
487
|
-
// Bridge has no cached token for this agent
|
|
488
|
-
return {
|
|
489
|
-
ok: false,
|
|
490
|
-
status: 404,
|
|
491
|
-
statusText: 'Not Found',
|
|
492
|
-
json: async () => ({ error: 'no cached token for agent' }),
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
ok: false,
|
|
497
|
-
status: 401,
|
|
498
|
-
statusText: 'Unauthorized',
|
|
499
|
-
};
|
|
500
|
-
};
|
|
501
|
-
await assert.rejects(() => client.callTool('get_profile', { agentId: 'agent-unknown' }), /MCP tool get_profile call failed: 401 Unauthorized/);
|
|
502
|
-
// 1 initial MCP call + 1 refresh attempt (404) + 0 retries
|
|
503
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
|
|
504
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
|
|
505
|
-
assert.equal(config.runtimeAccessToken, 'stale-token');
|
|
506
|
-
});
|
|
507
|
-
it('does not retry 401 after bridge refresh returns same token', async () => {
|
|
508
|
-
const config = {
|
|
509
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
510
|
-
heartbeatInterval: 15000,
|
|
511
|
-
reconnectInterval: 200,
|
|
512
|
-
maxReconnectAttempts: -1,
|
|
513
|
-
runtimeAccessToken: 'stale-token',
|
|
514
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
515
|
-
};
|
|
516
|
-
const client = new HttpClient(config);
|
|
517
|
-
global.fetch = async (url, options) => {
|
|
518
|
-
fetchCalls.push([url.toString(), options]);
|
|
519
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
520
|
-
// Bridge returns same token
|
|
521
|
-
return {
|
|
522
|
-
ok: true,
|
|
523
|
-
status: 200,
|
|
524
|
-
statusText: 'OK',
|
|
525
|
-
json: async () => ({ runtimeAccessToken: 'stale-token' }),
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
ok: false,
|
|
530
|
-
status: 401,
|
|
531
|
-
statusText: 'Unauthorized',
|
|
532
|
-
};
|
|
533
|
-
};
|
|
534
|
-
await assert.rejects(() => client.callTool('my_task', { agentId: 'agent-1' }), /MCP tool my_task call failed: 401 Unauthorized/);
|
|
535
|
-
// 1 initial MCP call + 1 refresh + 0 retries (same token, not updated)
|
|
536
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:8080/mcp/call').length, 1);
|
|
537
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
|
|
538
|
-
});
|
|
539
|
-
it('retries refresh on second call if first call 401 with different fresh token', async () => {
|
|
540
|
-
const config = {
|
|
541
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
542
|
-
heartbeatInterval: 15000,
|
|
543
|
-
reconnectInterval: 200,
|
|
544
|
-
maxReconnectAttempts: -1,
|
|
545
|
-
userId: 'user-1',
|
|
546
|
-
runtimeAccessToken: 'stale-token',
|
|
547
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
548
|
-
};
|
|
549
|
-
const client = new HttpClient(config);
|
|
550
|
-
let refreshCount = 0;
|
|
551
|
-
global.fetch = async (url, options) => {
|
|
552
|
-
fetchCalls.push([url.toString(), options]);
|
|
553
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
554
|
-
refreshCount += 1;
|
|
555
|
-
return {
|
|
556
|
-
ok: true,
|
|
557
|
-
status: 200,
|
|
558
|
-
statusText: 'OK',
|
|
559
|
-
json: async () => ({ runtimeAccessToken: `fresh-token-${refreshCount}` }),
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
const token = options.headers['X-Runtime-Token'];
|
|
563
|
-
if (token && token.startsWith('fresh-token-')) {
|
|
564
|
-
return {
|
|
565
|
-
ok: false,
|
|
566
|
-
status: 401,
|
|
567
|
-
statusText: 'Unauthorized',
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
return {
|
|
571
|
-
ok: false,
|
|
572
|
-
status: 401,
|
|
573
|
-
statusText: 'Unauthorized',
|
|
574
|
-
};
|
|
575
|
-
};
|
|
576
|
-
await assert.rejects(() => client.callTool('discover_group_rooms', { agentId: 'agent-1' }), /MCP tool discover_group_rooms call failed: 401 Unauthorized/);
|
|
577
|
-
// Second call also tries refresh
|
|
578
|
-
await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message call failed: 401 Unauthorized/);
|
|
579
|
-
assert.equal(config.runtimeAccessToken, 'fresh-token-2');
|
|
580
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 2);
|
|
581
|
-
});
|
|
582
|
-
it('shares one refresh promise among concurrent 401 calls', async () => {
|
|
583
|
-
const config = {
|
|
584
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
585
|
-
heartbeatInterval: 15000,
|
|
586
|
-
reconnectInterval: 200,
|
|
587
|
-
maxReconnectAttempts: -1,
|
|
588
|
-
userId: 'user-1',
|
|
589
|
-
runtimeAccessToken: 'stale-token',
|
|
590
|
-
runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
|
|
591
|
-
};
|
|
592
|
-
const client = new HttpClient(config);
|
|
593
|
-
let resolveRefresh = () => { };
|
|
594
|
-
const refreshGate = new Promise((resolve) => {
|
|
595
|
-
resolveRefresh = resolve;
|
|
596
|
-
});
|
|
597
|
-
let refreshCallCount = 0;
|
|
598
|
-
let freshTokenMcpCallCount = 0;
|
|
599
|
-
global.fetch = async (url, options) => {
|
|
600
|
-
fetchCalls.push([url.toString(), options]);
|
|
601
|
-
if (url.toString() === 'http://127.0.0.1:18081/runtime/binding/refresh-token') {
|
|
602
|
-
refreshCallCount += 1;
|
|
603
|
-
await refreshGate;
|
|
604
|
-
return {
|
|
605
|
-
ok: true,
|
|
606
|
-
status: 200,
|
|
607
|
-
statusText: 'OK',
|
|
608
|
-
json: async () => ({ runtimeAccessToken: 'fresh-but-rejected-token' }),
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
const requestHeaders = (options?.headers ?? {});
|
|
612
|
-
if (requestHeaders.Authorization === 'Bearer fresh-but-rejected-token') {
|
|
613
|
-
freshTokenMcpCallCount += 1;
|
|
614
|
-
}
|
|
615
|
-
return {
|
|
616
|
-
ok: false,
|
|
617
|
-
status: 401,
|
|
618
|
-
statusText: 'Unauthorized',
|
|
619
|
-
};
|
|
620
|
-
};
|
|
621
|
-
const firstCall = client.callTool('discover_group_rooms', { agentId: 'agent-1' });
|
|
622
|
-
// Wait for the refresh to start
|
|
623
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
624
|
-
// Second call should share the same refresh promise
|
|
625
|
-
const secondCall = client.callTool('poll_message', { agentId: 'agent-1' });
|
|
626
|
-
resolveRefresh();
|
|
627
|
-
await assert.rejects(() => firstCall, /MCP tool discover_group_rooms call failed: 401 Unauthorized/);
|
|
628
|
-
await assert.rejects(() => secondCall, /MCP tool poll_message call failed: 401 Unauthorized/);
|
|
629
|
-
assert.equal(config.runtimeAccessToken, 'fresh-but-rejected-token');
|
|
630
|
-
// Only one refresh call to bridge
|
|
631
|
-
assert.equal(refreshCallCount, 1);
|
|
632
|
-
assert.equal(fetchCalls.filter(([url]) => url === 'http://127.0.0.1:18081/runtime/binding/refresh-token').length, 1);
|
|
633
|
-
// Both tools retried once with fresh token
|
|
634
|
-
assert.equal(freshTokenMcpCallCount, 2);
|
|
635
|
-
});
|
|
636
|
-
it('handles 404 not found response', async () => {
|
|
637
|
-
const config = {
|
|
638
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
639
|
-
heartbeatInterval: 15000,
|
|
640
|
-
reconnectInterval: 200,
|
|
641
|
-
maxReconnectAttempts: -1,
|
|
642
|
-
};
|
|
643
|
-
const client = new HttpClient(config);
|
|
644
|
-
global.fetch = async () => ({
|
|
645
|
-
ok: false,
|
|
646
|
-
status: 404,
|
|
647
|
-
statusText: 'Not Found',
|
|
648
|
-
});
|
|
649
|
-
await assert.rejects(() => client.callTool('unknown-tool', {}), /MCP tool unknown-tool call failed: 404 Not Found/);
|
|
650
|
-
});
|
|
651
|
-
it('handles network errors', async () => {
|
|
652
|
-
const config = {
|
|
653
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
654
|
-
heartbeatInterval: 15000,
|
|
655
|
-
reconnectInterval: 200,
|
|
656
|
-
maxReconnectAttempts: -1,
|
|
657
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
658
|
-
};
|
|
659
|
-
const client = new HttpClient(config);
|
|
660
|
-
global.fetch = async () => {
|
|
661
|
-
throw new Error('Network error');
|
|
662
|
-
};
|
|
663
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /Network error/);
|
|
664
|
-
});
|
|
665
|
-
it('aborts MCP tool requests after configured timeout', async () => {
|
|
666
|
-
const config = {
|
|
667
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
668
|
-
heartbeatInterval: 15000,
|
|
669
|
-
reconnectInterval: 200,
|
|
670
|
-
maxReconnectAttempts: -1,
|
|
671
|
-
mcpHttpRequestTimeoutMs: 10,
|
|
672
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
673
|
-
};
|
|
674
|
-
const client = new HttpClient(config);
|
|
675
|
-
global.fetch = async (url, options) => {
|
|
676
|
-
fetchCalls.push([url.toString(), options]);
|
|
677
|
-
return await new Promise((_resolve, reject) => {
|
|
678
|
-
options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
|
|
679
|
-
});
|
|
680
|
-
};
|
|
681
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool timed out after 10ms/);
|
|
682
|
-
assert.equal(fetchCalls.length, 1);
|
|
683
|
-
assert.ok(fetchCalls[0][1].signal instanceof AbortSignal);
|
|
684
|
-
});
|
|
685
|
-
it('aborts poll_message requests after configured timeout', async () => {
|
|
686
|
-
const config = {
|
|
687
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
688
|
-
heartbeatInterval: 15000,
|
|
689
|
-
reconnectInterval: 200,
|
|
690
|
-
maxReconnectAttempts: -1,
|
|
691
|
-
runtimeAccessToken: 'runtime-token',
|
|
692
|
-
mcpHttpRequestTimeoutMs: 10,
|
|
693
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
694
|
-
};
|
|
695
|
-
const client = new HttpClient(config);
|
|
696
|
-
global.fetch = async (url, options) => {
|
|
697
|
-
fetchCalls.push([url.toString(), options]);
|
|
698
|
-
return await new Promise((_resolve, reject) => {
|
|
699
|
-
options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
|
|
700
|
-
});
|
|
701
|
-
};
|
|
702
|
-
await assert.rejects(() => client.callTool('poll_message', { agentId: 'agent-1' }), /MCP tool poll_message timed out after 10ms/);
|
|
703
|
-
assert.equal(fetchCalls.length, 1);
|
|
704
|
-
assert.ok(fetchCalls[0][1].signal instanceof AbortSignal);
|
|
705
|
-
});
|
|
706
|
-
it('retries on 5xx response until deadline exhausted', async () => {
|
|
707
|
-
const config = {
|
|
708
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
709
|
-
heartbeatInterval: 15000,
|
|
710
|
-
reconnectInterval: 200,
|
|
711
|
-
maxReconnectAttempts: -1,
|
|
712
|
-
mcpRetryIntervalMs: 1,
|
|
713
|
-
mcpRetryMaxAccumulatedMs: 20,
|
|
714
|
-
};
|
|
715
|
-
const client = new HttpClient(config);
|
|
716
|
-
global.fetch = async (url, options) => {
|
|
717
|
-
fetchCalls.push([url.toString(), options]);
|
|
718
|
-
return {
|
|
719
|
-
ok: false,
|
|
720
|
-
status: 503,
|
|
721
|
-
statusText: 'Service Unavailable',
|
|
722
|
-
};
|
|
723
|
-
};
|
|
724
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 503 Service Unavailable/);
|
|
725
|
-
// Multiple fetch calls due to retries
|
|
726
|
-
assert.ok(fetchCalls.length > 1);
|
|
727
|
-
});
|
|
728
|
-
it('retries on network error until deadline exhausted', async () => {
|
|
729
|
-
const config = {
|
|
730
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
731
|
-
heartbeatInterval: 15000,
|
|
732
|
-
reconnectInterval: 200,
|
|
733
|
-
maxReconnectAttempts: -1,
|
|
734
|
-
mcpRetryIntervalMs: 1,
|
|
735
|
-
mcpRetryMaxAccumulatedMs: 20,
|
|
736
|
-
};
|
|
737
|
-
const client = new HttpClient(config);
|
|
738
|
-
global.fetch = async (url, options) => {
|
|
739
|
-
fetchCalls.push([url.toString(), options]);
|
|
740
|
-
throw new Error('Network error');
|
|
741
|
-
};
|
|
742
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /Network error/);
|
|
743
|
-
assert.ok(fetchCalls.length > 1);
|
|
744
|
-
});
|
|
745
|
-
it('retries on timeout until deadline exhausted', async () => {
|
|
746
|
-
const config = {
|
|
747
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
748
|
-
heartbeatInterval: 15000,
|
|
749
|
-
reconnectInterval: 200,
|
|
750
|
-
maxReconnectAttempts: -1,
|
|
751
|
-
mcpHttpRequestTimeoutMs: 10,
|
|
752
|
-
mcpRetryIntervalMs: 1,
|
|
753
|
-
mcpRetryMaxAccumulatedMs: 30,
|
|
754
|
-
};
|
|
755
|
-
const client = new HttpClient(config);
|
|
756
|
-
global.fetch = async (url, options) => {
|
|
757
|
-
fetchCalls.push([url.toString(), options]);
|
|
758
|
-
return await new Promise((_resolve, reject) => {
|
|
759
|
-
options?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
|
|
760
|
-
});
|
|
761
|
-
};
|
|
762
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool timed out after 10ms/);
|
|
763
|
-
// Multiple fetch calls due to retries
|
|
764
|
-
assert.ok(fetchCalls.length > 1);
|
|
765
|
-
});
|
|
766
|
-
it('does not retry on 404 client error', async () => {
|
|
767
|
-
const config = {
|
|
768
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
769
|
-
heartbeatInterval: 15000,
|
|
770
|
-
reconnectInterval: 200,
|
|
771
|
-
maxReconnectAttempts: -1,
|
|
772
|
-
mcpRetryIntervalMs: 100,
|
|
773
|
-
mcpRetryMaxAccumulatedMs: 30000,
|
|
774
|
-
};
|
|
775
|
-
const client = new HttpClient(config);
|
|
776
|
-
global.fetch = async (url, options) => {
|
|
777
|
-
fetchCalls.push([url.toString(), options]);
|
|
778
|
-
return {
|
|
779
|
-
ok: false,
|
|
780
|
-
status: 404,
|
|
781
|
-
statusText: 'Not Found',
|
|
782
|
-
};
|
|
783
|
-
};
|
|
784
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool call failed: 404 Not Found/);
|
|
785
|
-
// Only 1 fetch call — no retry on 4xx
|
|
786
|
-
assert.equal(fetchCalls.length, 1);
|
|
787
|
-
});
|
|
788
|
-
it('does not retry on business error in response body', async () => {
|
|
789
|
-
const config = {
|
|
790
|
-
serverUrl: 'ws://localhost:8080/ws/agent',
|
|
791
|
-
heartbeatInterval: 15000,
|
|
792
|
-
reconnectInterval: 200,
|
|
793
|
-
maxReconnectAttempts: -1,
|
|
794
|
-
mcpRetryMaxAccumulatedMs: 0,
|
|
795
|
-
};
|
|
796
|
-
const client = new HttpClient(config);
|
|
797
|
-
global.fetch = async (url, options) => {
|
|
798
|
-
fetchCalls.push([url.toString(), options]);
|
|
799
|
-
return {
|
|
800
|
-
ok: true,
|
|
801
|
-
json: async () => ({ error: 'Tool execution failed' }),
|
|
802
|
-
};
|
|
803
|
-
};
|
|
804
|
-
await assert.rejects(() => client.callTool('test-tool', {}), /MCP tool test-tool failed: Tool execution failed/);
|
|
805
|
-
assert.equal(fetchCalls.length, 1);
|
|
806
|
-
});
|
|
807
|
-
});
|
|
808
|
-
});
|