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,1066 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
|
3
|
-
import { AgentClient, PollLongPollTimeoutError } from "./agent-client.js";
|
|
4
|
-
import { setMcpNodeLogSink } from "./logger.js";
|
|
5
|
-
// Helper function to create a complete ProfileInfo
|
|
6
|
-
function createProfileInfo(overrides = {}) {
|
|
7
|
-
return {
|
|
8
|
-
id: "agent-1",
|
|
9
|
-
displayName: "Agent One",
|
|
10
|
-
roleName: "Backend Agent",
|
|
11
|
-
prompt: "backend development",
|
|
12
|
-
isOnline: true,
|
|
13
|
-
onlineSince: new Date().toISOString(),
|
|
14
|
-
createdAt: new Date().toISOString(),
|
|
15
|
-
...overrides,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
describe("AgentClient", () => {
|
|
19
|
-
const originalEnv = { ...process.env };
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
delete process.env.AWS_SERVER_URL;
|
|
22
|
-
delete process.env.AWS_HEARTBEAT_INTERVAL;
|
|
23
|
-
delete process.env.AWS_WORKSPACE_PATH;
|
|
24
|
-
delete process.env.AWS_ROLE_NAME;
|
|
25
|
-
delete process.env.AWS_AGENT_ID;
|
|
26
|
-
delete process.env.AWS_PROJECT_NAME;
|
|
27
|
-
delete process.env.AWS_PROMPT;
|
|
28
|
-
delete process.env.AWS_MCP_HTTP_URL;
|
|
29
|
-
delete process.env.AWS_RUNTIME_BRIDGE_BASE_URL;
|
|
30
|
-
});
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
process.env = { ...originalEnv };
|
|
33
|
-
mock.restoreAll();
|
|
34
|
-
});
|
|
35
|
-
describe("initialize", () => {
|
|
36
|
-
it("should allow preset agentId without direct env token when no launch binding is available", async () => {
|
|
37
|
-
process.env.AWS_AGENT_ID = "preset-agent-without-token";
|
|
38
|
-
process.env.AWS_RUNTIME_BRIDGE_BASE_URL = "http://127.0.0.1:18081";
|
|
39
|
-
mock.method(globalThis, "fetch", async () => new Response("", { status: 404 }));
|
|
40
|
-
const client = new AgentClient();
|
|
41
|
-
await client.initialize();
|
|
42
|
-
assert.equal(client.getAgentConfig().agentId, "preset-agent-without-token");
|
|
43
|
-
});
|
|
44
|
-
it("should allow preset agentId when launch binding provides runtime token", async () => {
|
|
45
|
-
process.env.AWS_AGENT_ID = "preset-agent-with-token";
|
|
46
|
-
process.env.AWS_RUNTIME_BRIDGE_BASE_URL = "http://127.0.0.1:18081";
|
|
47
|
-
mock.method(globalThis, "fetch", async () => new Response(JSON.stringify({
|
|
48
|
-
agentId: "preset-agent-with-token",
|
|
49
|
-
workspacePath: "/workspace/demo",
|
|
50
|
-
runtimeAccessToken: "agent-token-1234567890",
|
|
51
|
-
schedulerBaseUrl: "http://scheduler:7380",
|
|
52
|
-
}), { status: 200, headers: { "Content-Type": "application/json" } }));
|
|
53
|
-
const client = new AgentClient();
|
|
54
|
-
await client.initialize();
|
|
55
|
-
assert.equal(client.getAgentConfig().agentId, "preset-agent-with-token");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe("initialize with unreachable bridge", () => {
|
|
59
|
-
function setupFastClaimEnv() {
|
|
60
|
-
process.env.AWS_AGENT_ID = "preset-agent-retry";
|
|
61
|
-
process.env.AWS_RUNTIME_BRIDGE_BASE_URL = "http://127.0.0.1:18081";
|
|
62
|
-
process.env.AWS_MCP_LAUNCH_BINDING_CLAIM_ATTEMPTS = "1";
|
|
63
|
-
process.env.AWS_MCP_LAUNCH_BINDING_CLAIM_RETRY_DELAY_MS = "0";
|
|
64
|
-
}
|
|
65
|
-
it("keeps bridge base url and pends claim when bridge is unreachable at startup", async () => {
|
|
66
|
-
setupFastClaimEnv();
|
|
67
|
-
mock.method(globalThis, "fetch", async () => {
|
|
68
|
-
throw new Error("fetch failed");
|
|
69
|
-
});
|
|
70
|
-
const client = new AgentClient();
|
|
71
|
-
await client.initialize();
|
|
72
|
-
// 关键回归点:不能清空 runtimeBridgeBaseUrl,否则后续永远拿不到 token,
|
|
73
|
-
// 日志目录会停留在 unknown-role-* 且所有工具调用 401。
|
|
74
|
-
assert.equal(client.getServerConfig().runtimeBridgeBaseUrl, "http://127.0.0.1:18081");
|
|
75
|
-
});
|
|
76
|
-
it("applies reclaimed launch binding once bridge recovers", async () => {
|
|
77
|
-
setupFastClaimEnv();
|
|
78
|
-
let bridgeUp = false;
|
|
79
|
-
mock.method(globalThis, "fetch", async (url) => {
|
|
80
|
-
const urlText = url.toString();
|
|
81
|
-
if (!urlText.includes("/runtime/binding/claim-agent")) {
|
|
82
|
-
throw new Error(`unexpected fetch target: ${urlText}`);
|
|
83
|
-
}
|
|
84
|
-
if (!bridgeUp) {
|
|
85
|
-
throw new Error("fetch failed");
|
|
86
|
-
}
|
|
87
|
-
return new Response(JSON.stringify({
|
|
88
|
-
agentId: "preset-agent-retry",
|
|
89
|
-
workspacePath: "/workspace/demo",
|
|
90
|
-
runtimeAccessToken: "reclaimed-token-1234567890",
|
|
91
|
-
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
92
|
-
});
|
|
93
|
-
const client = new AgentClient();
|
|
94
|
-
await client.initialize();
|
|
95
|
-
assert.equal(client.getServerConfig().runtimeAccessToken, undefined);
|
|
96
|
-
bridgeUp = true;
|
|
97
|
-
// register() 每次尝试前会走同一条补认领路径;这里直接调用私有方法聚焦断言。
|
|
98
|
-
await Reflect.get(client, "retryPendingLaunchBindingClaim").call(client);
|
|
99
|
-
assert.equal(client.getServerConfig().runtimeAccessToken, "reclaimed-token-1234567890");
|
|
100
|
-
});
|
|
101
|
-
it("still registers via preset agent id while bridge stays unreachable", async () => {
|
|
102
|
-
setupFastClaimEnv();
|
|
103
|
-
mock.method(globalThis, "fetch", async () => {
|
|
104
|
-
throw new Error("fetch failed");
|
|
105
|
-
});
|
|
106
|
-
const client = new AgentClient();
|
|
107
|
-
await client.initialize();
|
|
108
|
-
let getProfileCalls = 0;
|
|
109
|
-
Reflect.set(client, "httpClient", {
|
|
110
|
-
callTool: async (tool) => {
|
|
111
|
-
if (tool !== "get_profile") {
|
|
112
|
-
throw new Error(`unexpected tool: ${tool}`);
|
|
113
|
-
}
|
|
114
|
-
getProfileCalls += 1;
|
|
115
|
-
return createProfileInfo({ id: "preset-agent-retry" });
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
Reflect.set(client, "wsClient", {
|
|
119
|
-
connect: async () => { },
|
|
120
|
-
disconnect: () => { },
|
|
121
|
-
isConnected: () => true,
|
|
122
|
-
on: () => { },
|
|
123
|
-
});
|
|
124
|
-
const result = await client.register();
|
|
125
|
-
assert.equal(result.agentId, "preset-agent-retry");
|
|
126
|
-
assert.ok(getProfileCalls >= 1);
|
|
127
|
-
assert.equal(client.getServerConfig().runtimeBridgeBaseUrl, "http://127.0.0.1:18081");
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe("register", () => {
|
|
131
|
-
it("should register as new agent when no preset agentId", async () => {
|
|
132
|
-
process.env.AWS_ROLE_NAME = "test-role";
|
|
133
|
-
process.env.AWS_PROJECT_NAME = "test-project";
|
|
134
|
-
const client = new AgentClient();
|
|
135
|
-
const registerResult = {
|
|
136
|
-
agentId: "agent-new-123",
|
|
137
|
-
displayName: "test-role-A1B2",
|
|
138
|
-
status: "online",
|
|
139
|
-
projectId: "test-project",
|
|
140
|
-
};
|
|
141
|
-
Reflect.set(client, "httpClient", {
|
|
142
|
-
callTool: async () => registerResult,
|
|
143
|
-
});
|
|
144
|
-
Reflect.set(client, "wsClient", {
|
|
145
|
-
connect: async () => { },
|
|
146
|
-
disconnect: () => { },
|
|
147
|
-
isConnected: () => true,
|
|
148
|
-
on: () => { },
|
|
149
|
-
});
|
|
150
|
-
const result = await client.register();
|
|
151
|
-
assert.equal(result.agentId, "agent-new-123");
|
|
152
|
-
assert.equal(result.displayName, "test-role-A1B2");
|
|
153
|
-
assert.equal(result.status, "online");
|
|
154
|
-
});
|
|
155
|
-
it("should use preset agentId when provided", async () => {
|
|
156
|
-
process.env.AWS_AGENT_ID = "preset-agent-456";
|
|
157
|
-
process.env.AWS_ROLE_NAME = "backend-expert";
|
|
158
|
-
const client = new AgentClient();
|
|
159
|
-
const profileInfo = createProfileInfo({
|
|
160
|
-
id: "preset-agent-456",
|
|
161
|
-
displayName: "Backend Expert-X7Y8",
|
|
162
|
-
projectId: "demo",
|
|
163
|
-
workspacePath: "/workspace/demo",
|
|
164
|
-
});
|
|
165
|
-
Reflect.set(client, "httpClient", {
|
|
166
|
-
callTool: async (toolName) => {
|
|
167
|
-
if (toolName === "get_profile") {
|
|
168
|
-
return profileInfo;
|
|
169
|
-
}
|
|
170
|
-
return {};
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
Reflect.set(client, "wsClient", {
|
|
174
|
-
connect: async () => { },
|
|
175
|
-
disconnect: () => { },
|
|
176
|
-
isConnected: () => true,
|
|
177
|
-
on: () => { },
|
|
178
|
-
});
|
|
179
|
-
const result = await client.register();
|
|
180
|
-
assert.equal(result.agentId, "preset-agent-456");
|
|
181
|
-
});
|
|
182
|
-
it("should return online after reconnecting with preset agentId", async () => {
|
|
183
|
-
process.env.AWS_AGENT_ID = "preset-agent-456";
|
|
184
|
-
process.env.AWS_ROLE_NAME = "backend-expert";
|
|
185
|
-
const client = new AgentClient();
|
|
186
|
-
const offlineProfile = createProfileInfo({
|
|
187
|
-
id: "preset-agent-456",
|
|
188
|
-
displayName: "Backend Expert-X7Y8",
|
|
189
|
-
projectId: "demo",
|
|
190
|
-
workspacePath: "/workspace/demo",
|
|
191
|
-
isOnline: false,
|
|
192
|
-
onlineSince: null,
|
|
193
|
-
});
|
|
194
|
-
const onlineProfile = createProfileInfo({
|
|
195
|
-
id: "preset-agent-456",
|
|
196
|
-
displayName: "Backend Expert-X7Y8",
|
|
197
|
-
projectId: "demo",
|
|
198
|
-
workspacePath: "/workspace/demo",
|
|
199
|
-
isOnline: true,
|
|
200
|
-
});
|
|
201
|
-
let getProfileCallCount = 0;
|
|
202
|
-
Reflect.set(client, "httpClient", {
|
|
203
|
-
callTool: async (toolName) => {
|
|
204
|
-
if (toolName === "get_profile") {
|
|
205
|
-
getProfileCallCount += 1;
|
|
206
|
-
return getProfileCallCount === 1 ? offlineProfile : onlineProfile;
|
|
207
|
-
}
|
|
208
|
-
return {};
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
let connectCalled = false;
|
|
212
|
-
Reflect.set(client, "wsClient", {
|
|
213
|
-
connect: async () => {
|
|
214
|
-
connectCalled = true;
|
|
215
|
-
},
|
|
216
|
-
disconnect: () => { },
|
|
217
|
-
isConnected: () => true,
|
|
218
|
-
send: () => true,
|
|
219
|
-
on: () => { },
|
|
220
|
-
});
|
|
221
|
-
const result = await client.register();
|
|
222
|
-
assert.equal(connectCalled, true);
|
|
223
|
-
assert.equal(getProfileCallCount, 2);
|
|
224
|
-
assert.equal(result.agentId, "preset-agent-456");
|
|
225
|
-
assert.equal(result.status, "online");
|
|
226
|
-
assert.equal(result.projectId, "demo");
|
|
227
|
-
assert.equal(result.workspacePath, "/workspace/demo");
|
|
228
|
-
});
|
|
229
|
-
it("should throw error when registration fails without agentId", async () => {
|
|
230
|
-
process.env.AWS_ROLE_NAME = "test-role";
|
|
231
|
-
const client = new AgentClient();
|
|
232
|
-
Reflect.set(client, "httpClient", {
|
|
233
|
-
callTool: async () => ({}),
|
|
234
|
-
});
|
|
235
|
-
await assert.rejects(async () => await client.register(), /agentId was not returned/);
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
describe("unregister", () => {
|
|
239
|
-
it("should call unregister tool and disconnect", async () => {
|
|
240
|
-
const client = new AgentClient();
|
|
241
|
-
Reflect.set(client, "agentId", "agent-to-unregister");
|
|
242
|
-
let unregisterCalled = false;
|
|
243
|
-
Reflect.set(client, "httpClient", {
|
|
244
|
-
callTool: async (toolName) => {
|
|
245
|
-
if (toolName === "unregister") {
|
|
246
|
-
unregisterCalled = true;
|
|
247
|
-
}
|
|
248
|
-
return {};
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
let disconnectCalled = false;
|
|
252
|
-
Reflect.set(client, "wsClient", {
|
|
253
|
-
disconnect: () => {
|
|
254
|
-
disconnectCalled = true;
|
|
255
|
-
},
|
|
256
|
-
isConnected: () => false,
|
|
257
|
-
});
|
|
258
|
-
await client.unregister();
|
|
259
|
-
assert.equal(unregisterCalled, true);
|
|
260
|
-
assert.equal(disconnectCalled, true);
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
describe("callAgentScopedTool", () => {
|
|
264
|
-
it("should always use the registered agent id over a caller-supplied id", async () => {
|
|
265
|
-
const client = new AgentClient();
|
|
266
|
-
Reflect.set(client, "agentId", "registered-agent");
|
|
267
|
-
let capturedArgs;
|
|
268
|
-
Reflect.set(client, "httpClient", {
|
|
269
|
-
callTool: async (_toolName, args) => {
|
|
270
|
-
capturedArgs = args;
|
|
271
|
-
return { success: true };
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
await client.callAgentScopedTool("meeting_speak", {
|
|
275
|
-
agentId: "spoofed-agent",
|
|
276
|
-
meetingId: "meeting-1",
|
|
277
|
-
content: "hello",
|
|
278
|
-
});
|
|
279
|
-
assert.equal(capturedArgs?.agentId, "registered-agent");
|
|
280
|
-
assert.equal(capturedArgs?.meetingId, "meeting-1");
|
|
281
|
-
assert.equal(capturedArgs?.content, "hello");
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
describe("hire identity injection", () => {
|
|
285
|
-
it("hireColleague should always use the registered agent id over a caller-supplied id", async () => {
|
|
286
|
-
const client = new AgentClient();
|
|
287
|
-
Reflect.set(client, "agentId", "registered-agent");
|
|
288
|
-
let capturedArgs;
|
|
289
|
-
Reflect.set(client, "httpClient", {
|
|
290
|
-
callTool: async (_toolName, args) => {
|
|
291
|
-
capturedArgs = args;
|
|
292
|
-
return { success: true };
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
await client.hireColleague({
|
|
296
|
-
agentId: "spoofed-agent",
|
|
297
|
-
workspacePath: "/tmp/ws",
|
|
298
|
-
});
|
|
299
|
-
assert.equal(capturedArgs?.agentId, "registered-agent");
|
|
300
|
-
assert.equal(capturedArgs?.workspacePath, "/tmp/ws");
|
|
301
|
-
});
|
|
302
|
-
it("hireColleaguesBatch should always use the registered agent id over a caller-supplied id", async () => {
|
|
303
|
-
const client = new AgentClient();
|
|
304
|
-
Reflect.set(client, "agentId", "registered-agent");
|
|
305
|
-
let capturedArgs;
|
|
306
|
-
Reflect.set(client, "httpClient", {
|
|
307
|
-
callTool: async (_toolName, args) => {
|
|
308
|
-
capturedArgs = args;
|
|
309
|
-
return { success: true };
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
await client.hireColleaguesBatch({
|
|
313
|
-
agentId: "spoofed-agent",
|
|
314
|
-
items: [{ workspacePath: "/tmp/ws" }],
|
|
315
|
-
});
|
|
316
|
-
assert.equal(capturedArgs?.agentId, "registered-agent");
|
|
317
|
-
assert.deepEqual(capturedArgs?.items, [{ workspacePath: "/tmp/ws" }]);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
describe("sendGroupMessage", () => {
|
|
321
|
-
it("should send group message with correct parameters", async () => {
|
|
322
|
-
const client = new AgentClient();
|
|
323
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
324
|
-
Reflect.set(client, "projectId", "demo");
|
|
325
|
-
let capturedArgs;
|
|
326
|
-
Reflect.set(client, "httpClient", {
|
|
327
|
-
callTool: async (_toolName, args) => {
|
|
328
|
-
capturedArgs = args;
|
|
329
|
-
return { msgId: 1, timestamp: new Date().toISOString() };
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
await client.sendGroupMessage("hello group", "demo", "project:demo");
|
|
333
|
-
assert.equal(capturedArgs?.agentId, "agent-1");
|
|
334
|
-
assert.equal(capturedArgs?.projectId, "demo");
|
|
335
|
-
assert.equal(capturedArgs?.roomId, "project:demo");
|
|
336
|
-
assert.equal(capturedArgs?.content, "hello group");
|
|
337
|
-
});
|
|
338
|
-
it("should throw when not registered", async () => {
|
|
339
|
-
const client = new AgentClient();
|
|
340
|
-
await assert.rejects(async () => await client.sendGroupMessage("hello"), /Agent is not registered/);
|
|
341
|
-
});
|
|
342
|
-
});
|
|
343
|
-
describe("sendDirectMessage", () => {
|
|
344
|
-
it("should send direct message with correct parameters", async () => {
|
|
345
|
-
const client = new AgentClient();
|
|
346
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
347
|
-
let capturedArgs;
|
|
348
|
-
const mockCallToolDm = async (_toolName, args) => {
|
|
349
|
-
capturedArgs = args;
|
|
350
|
-
return { msgId: "dm-1", timestamp: new Date().toISOString() };
|
|
351
|
-
};
|
|
352
|
-
Reflect.set(client, "httpClient", {
|
|
353
|
-
callTool: mockCallToolDm,
|
|
354
|
-
callToolOnce: mockCallToolDm,
|
|
355
|
-
});
|
|
356
|
-
await client.sendDirectMessage("target-user", "hello dm", true, undefined, 30000);
|
|
357
|
-
assert.equal(capturedArgs?.agentId, "agent-1");
|
|
358
|
-
assert.equal(capturedArgs?.targetId, "target-user");
|
|
359
|
-
assert.equal(capturedArgs?.content, "hello dm");
|
|
360
|
-
assert.equal(capturedArgs?.requireReply, true);
|
|
361
|
-
assert.equal(capturedArgs?.timeoutMs, 30000);
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
describe("sendCallAndWaitReply", () => {
|
|
365
|
-
it("should return reply when received", async () => {
|
|
366
|
-
const client = new AgentClient();
|
|
367
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
368
|
-
const replyMessage = {
|
|
369
|
-
msgId: "reply-1",
|
|
370
|
-
senderId: "target-user",
|
|
371
|
-
senderName: "Target User",
|
|
372
|
-
content: "this is reply",
|
|
373
|
-
requireReply: false,
|
|
374
|
-
timestamp: new Date().toISOString(),
|
|
375
|
-
};
|
|
376
|
-
const mockCallReply = async () => ({
|
|
377
|
-
msgId: "dm-1",
|
|
378
|
-
reply: replyMessage,
|
|
379
|
-
});
|
|
380
|
-
Reflect.set(client, "httpClient", {
|
|
381
|
-
callTool: mockCallReply,
|
|
382
|
-
callToolOnce: mockCallReply,
|
|
383
|
-
});
|
|
384
|
-
const result = await client.sendCallAndWaitReply("target-user", "hello", 30000);
|
|
385
|
-
assert.equal(result.msgId, "reply-1");
|
|
386
|
-
assert.equal(result.content, "this is reply");
|
|
387
|
-
});
|
|
388
|
-
it("should throw when no reply received", async () => {
|
|
389
|
-
const client = new AgentClient();
|
|
390
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
391
|
-
const mockCallNoReply = async () => ({
|
|
392
|
-
msgId: "dm-1",
|
|
393
|
-
reply: null,
|
|
394
|
-
});
|
|
395
|
-
Reflect.set(client, "httpClient", {
|
|
396
|
-
callTool: mockCallNoReply,
|
|
397
|
-
callToolOnce: mockCallNoReply,
|
|
398
|
-
});
|
|
399
|
-
await assert.rejects(async () => await client.sendCallAndWaitReply("target-user", "hello"), /No reply received/);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
describe("discoverColleagues", () => {
|
|
403
|
-
it("should call discover_colleague tool", async () => {
|
|
404
|
-
const client = new AgentClient();
|
|
405
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
406
|
-
const expectedResponse = {
|
|
407
|
-
humans: [],
|
|
408
|
-
agents: [
|
|
409
|
-
{ id: "agent-2", displayName: "Agent Two", roleName: "Backend" },
|
|
410
|
-
],
|
|
411
|
-
};
|
|
412
|
-
let capturedArgs;
|
|
413
|
-
Reflect.set(client, "httpClient", {
|
|
414
|
-
callTool: async (_toolName, args) => {
|
|
415
|
-
capturedArgs = args;
|
|
416
|
-
return expectedResponse;
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
const result = await client.discoverColleagues();
|
|
420
|
-
assert.equal(capturedArgs?.agentId, "agent-1");
|
|
421
|
-
assert.deepEqual(result, expectedResponse);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
describe("fetchUnreadMessages", () => {
|
|
425
|
-
it("should call poll_message when pipeline task notifications are enabled", async () => {
|
|
426
|
-
const client = new AgentClient();
|
|
427
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
428
|
-
Reflect.set(client, "projectId", "demo");
|
|
429
|
-
const calledTools = [];
|
|
430
|
-
const mockCallPoll1 = async (toolName) => {
|
|
431
|
-
calledTools.push(toolName);
|
|
432
|
-
if (toolName === "poll_message") {
|
|
433
|
-
return { messages: [] };
|
|
434
|
-
}
|
|
435
|
-
if (toolName === "discover_group_rooms") {
|
|
436
|
-
return { groups: [] };
|
|
437
|
-
}
|
|
438
|
-
return {};
|
|
439
|
-
};
|
|
440
|
-
Reflect.set(client, "httpClient", {
|
|
441
|
-
callTool: mockCallPoll1,
|
|
442
|
-
callToolOnce: mockCallPoll1,
|
|
443
|
-
});
|
|
444
|
-
await client.fetchUnreadMessages({ includePipelineTasks: true });
|
|
445
|
-
assert.equal(calledTools[0], "poll_message");
|
|
446
|
-
assert.equal(calledTools.includes("get_dm_messages"), false);
|
|
447
|
-
});
|
|
448
|
-
it("should recover server poll_message 503 as empty result for local retry", async () => {
|
|
449
|
-
const client = new AgentClient();
|
|
450
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
451
|
-
Reflect.set(client, "projectId", "demo");
|
|
452
|
-
const calledTools = [];
|
|
453
|
-
const mockCallPoll2 = async (toolName) => {
|
|
454
|
-
calledTools.push(toolName);
|
|
455
|
-
if (toolName === "poll_message") {
|
|
456
|
-
throw new Error('MCP tool poll_message call failed: 503 Service Unavailable - {"error":"Async request timed out"}');
|
|
457
|
-
}
|
|
458
|
-
if (toolName === "discover_group_rooms") {
|
|
459
|
-
return { groups: [] };
|
|
460
|
-
}
|
|
461
|
-
return {};
|
|
462
|
-
};
|
|
463
|
-
Reflect.set(client, "httpClient", {
|
|
464
|
-
callTool: mockCallPoll2,
|
|
465
|
-
callToolOnce: mockCallPoll2,
|
|
466
|
-
});
|
|
467
|
-
const result = await client.fetchUnreadMessages({ includePipelineTasks: true });
|
|
468
|
-
assert.deepEqual(result, { directMessages: [], groupMessages: [] });
|
|
469
|
-
assert.deepEqual(calledTools, ["poll_message", "discover_group_rooms"]);
|
|
470
|
-
});
|
|
471
|
-
it("should recover client poll_message timeout as empty result for local retry", async () => {
|
|
472
|
-
const client = new AgentClient();
|
|
473
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
474
|
-
Reflect.set(client, "projectId", "demo");
|
|
475
|
-
const calledTools = [];
|
|
476
|
-
const mockCallPoll3 = async (toolName) => {
|
|
477
|
-
calledTools.push(toolName);
|
|
478
|
-
if (toolName === "poll_message") {
|
|
479
|
-
throw new Error("MCP tool poll_message timed out after 120000ms");
|
|
480
|
-
}
|
|
481
|
-
if (toolName === "discover_group_rooms") {
|
|
482
|
-
return { groups: [] };
|
|
483
|
-
}
|
|
484
|
-
return {};
|
|
485
|
-
};
|
|
486
|
-
Reflect.set(client, "httpClient", {
|
|
487
|
-
callTool: mockCallPoll3,
|
|
488
|
-
callToolOnce: mockCallPoll3,
|
|
489
|
-
});
|
|
490
|
-
const result = await client.fetchUnreadMessages({ includePipelineTasks: true });
|
|
491
|
-
assert.deepEqual(result, { directMessages: [], groupMessages: [] });
|
|
492
|
-
assert.deepEqual(calledTools, ["poll_message", "discover_group_rooms"]);
|
|
493
|
-
});
|
|
494
|
-
it("should pass preemptiveJoinWaitMs and pollMessageTimeoutMs through to poll_message", async () => {
|
|
495
|
-
const client = new AgentClient();
|
|
496
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
497
|
-
Reflect.set(client, "projectId", "demo");
|
|
498
|
-
const capturedCalls = [];
|
|
499
|
-
Reflect.set(client, "httpClient", {
|
|
500
|
-
callTool: async () => ({}),
|
|
501
|
-
callToolOnce: async (toolName, args, timeoutMs) => {
|
|
502
|
-
capturedCalls.push({ toolName, args, timeoutMs });
|
|
503
|
-
if (toolName === "poll_message") {
|
|
504
|
-
return { messages: [] };
|
|
505
|
-
}
|
|
506
|
-
if (toolName === "discover_group_rooms") {
|
|
507
|
-
return { groups: [] };
|
|
508
|
-
}
|
|
509
|
-
return {};
|
|
510
|
-
},
|
|
511
|
-
});
|
|
512
|
-
await client.fetchUnreadMessages({
|
|
513
|
-
includePipelineTasks: true,
|
|
514
|
-
preemptiveJoinWaitMs: 0,
|
|
515
|
-
pollMessageTimeoutMs: 65_000,
|
|
516
|
-
skipGroupMessages: true,
|
|
517
|
-
});
|
|
518
|
-
const pollCall = capturedCalls.find((call) => call.toolName === "poll_message");
|
|
519
|
-
assert.ok(pollCall, "poll_message 应被调用");
|
|
520
|
-
// 显式 0 必须下发(语义为"立即加入但不阻塞等待"):省略会让服务端回退默认阻塞预算,
|
|
521
|
-
// 后台周期性 hydrate 在服务端堆积长阻塞 Callable
|
|
522
|
-
assert.equal(pollCall.args.preemptiveJoinWaitMs, 0);
|
|
523
|
-
// 会议邀请强制 hydrate 的单请求超时需覆盖服务端等待预算,否则合法长请求被误判超时
|
|
524
|
-
assert.equal(pollCall.timeoutMs, 65_000);
|
|
525
|
-
// 会议快速 hydrate 跳过群聊发现,避免群阶段耗时拖慢抢占返回
|
|
526
|
-
assert.equal(capturedCalls.some((call) => call.toolName === "discover_group_rooms"), false);
|
|
527
|
-
});
|
|
528
|
-
it("should propagate client poll_message timeout as PollLongPollTimeoutError when requested", async () => {
|
|
529
|
-
const client = new AgentClient();
|
|
530
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
531
|
-
Reflect.set(client, "projectId", "demo");
|
|
532
|
-
const mockCallPollTimeout = async (toolName) => {
|
|
533
|
-
if (toolName === "poll_message") {
|
|
534
|
-
throw new Error("MCP tool poll_message timed out after 65000ms");
|
|
535
|
-
}
|
|
536
|
-
if (toolName === "discover_group_rooms") {
|
|
537
|
-
return { groups: [] };
|
|
538
|
-
}
|
|
539
|
-
return {};
|
|
540
|
-
};
|
|
541
|
-
Reflect.set(client, "httpClient", {
|
|
542
|
-
callTool: mockCallPollTimeout,
|
|
543
|
-
callToolOnce: mockCallPollTimeout,
|
|
544
|
-
});
|
|
545
|
-
// 超时必须与"服务端快速返回空"区分开:超时说明服务端大概率仍在执行同一请求
|
|
546
|
-
// (Callable 占线至预算结束),立即重试只会在服务端叠加长阻塞线程
|
|
547
|
-
await assert.rejects(() => client.fetchUnreadMessages({
|
|
548
|
-
includePipelineTasks: true,
|
|
549
|
-
propagatePollTimeout: true,
|
|
550
|
-
}), (error) => {
|
|
551
|
-
assert.ok(error instanceof PollLongPollTimeoutError);
|
|
552
|
-
assert.match(error.message, /timed out after 65000ms/);
|
|
553
|
-
return true;
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
it("should propagate server poll_message 503 as PollLongPollTimeoutError when requested", async () => {
|
|
557
|
-
const client = new AgentClient();
|
|
558
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
559
|
-
Reflect.set(client, "projectId", "demo");
|
|
560
|
-
const mockCallPoll503 = async (toolName) => {
|
|
561
|
-
if (toolName === "poll_message") {
|
|
562
|
-
throw new Error('MCP tool poll_message call failed: 503 Service Unavailable - {"error":"Async request timed out"}');
|
|
563
|
-
}
|
|
564
|
-
if (toolName === "discover_group_rooms") {
|
|
565
|
-
return { groups: [] };
|
|
566
|
-
}
|
|
567
|
-
return {};
|
|
568
|
-
};
|
|
569
|
-
Reflect.set(client, "httpClient", {
|
|
570
|
-
callTool: mockCallPoll503,
|
|
571
|
-
callToolOnce: mockCallPoll503,
|
|
572
|
-
});
|
|
573
|
-
await assert.rejects(() => client.fetchUnreadMessages({
|
|
574
|
-
includePipelineTasks: true,
|
|
575
|
-
propagatePollTimeout: true,
|
|
576
|
-
}), PollLongPollTimeoutError);
|
|
577
|
-
});
|
|
578
|
-
it("should keep fetched DMs when discover_group_rooms fails in group phase", async () => {
|
|
579
|
-
const client = new AgentClient();
|
|
580
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
581
|
-
Reflect.set(client, "projectId", "demo");
|
|
582
|
-
const meetingNotice = {
|
|
583
|
-
msgId: 42,
|
|
584
|
-
channel: "meeting",
|
|
585
|
-
meetingId: "m-9",
|
|
586
|
-
senderId: "MEETING-SYSTEM",
|
|
587
|
-
senderName: "会议系统",
|
|
588
|
-
content: "系统已自动为你加入会议「评审会」。",
|
|
589
|
-
isSystem: true,
|
|
590
|
-
timestamp: new Date().toISOString(),
|
|
591
|
-
};
|
|
592
|
-
Reflect.set(client, "httpClient", {
|
|
593
|
-
callTool: async (toolName) => {
|
|
594
|
-
if (toolName === "discover_group_rooms") {
|
|
595
|
-
throw new Error("MCP tool discover_group_rooms call failed: 500 Internal Server Error");
|
|
596
|
-
}
|
|
597
|
-
return {};
|
|
598
|
-
},
|
|
599
|
-
callToolOnce: async (toolName) => {
|
|
600
|
-
if (toolName === "poll_message") {
|
|
601
|
-
return { messages: [meetingNotice] };
|
|
602
|
-
}
|
|
603
|
-
if (toolName === "discover_group_rooms") {
|
|
604
|
-
throw new Error("MCP tool discover_group_rooms call failed: 500 Internal Server Error");
|
|
605
|
-
}
|
|
606
|
-
return {};
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
// 群阶段失败必须降级为无群消息,绝不能抛出导致已取到的私信(含会议自动加入提示)被整体丢弃
|
|
610
|
-
const result = await client.fetchUnreadMessages({ includePipelineTasks: true });
|
|
611
|
-
assert.equal(result.directMessages.length, 1);
|
|
612
|
-
assert.deepEqual(result.directMessages[0], meetingNotice);
|
|
613
|
-
assert.deepEqual(result.groupMessages, []);
|
|
614
|
-
});
|
|
615
|
-
it("should not recover non-poll_message 503 errors", async () => {
|
|
616
|
-
const client = new AgentClient();
|
|
617
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
618
|
-
Reflect.set(client, "projectId", "demo");
|
|
619
|
-
Reflect.set(client, "httpClient", {
|
|
620
|
-
callTool: async (toolName) => {
|
|
621
|
-
if (toolName === "get_dm_messages") {
|
|
622
|
-
throw new Error("MCP tool get_dm_messages call failed: 503 Service Unavailable");
|
|
623
|
-
}
|
|
624
|
-
return {};
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
await assert.rejects(() => client.fetchUnreadMessages(), /MCP tool get_dm_messages call failed: 503 Service Unavailable/);
|
|
628
|
-
});
|
|
629
|
-
it("should not recover poll_message non-503 errors", async () => {
|
|
630
|
-
const client = new AgentClient();
|
|
631
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
632
|
-
Reflect.set(client, "projectId", "demo");
|
|
633
|
-
const mockCallPoll5 = async (toolName) => {
|
|
634
|
-
if (toolName === "poll_message") {
|
|
635
|
-
throw new Error("MCP tool poll_message call failed: 500 Internal Server Error");
|
|
636
|
-
}
|
|
637
|
-
return {};
|
|
638
|
-
};
|
|
639
|
-
Reflect.set(client, "httpClient", {
|
|
640
|
-
callTool: mockCallPoll5,
|
|
641
|
-
callToolOnce: mockCallPoll5,
|
|
642
|
-
});
|
|
643
|
-
await assert.rejects(() => client.fetchUnreadMessages({ includePipelineTasks: true }), /MCP tool poll_message call failed: 500 Internal Server Error/);
|
|
644
|
-
});
|
|
645
|
-
it("should keep get_dm_messages for non-claiming message fetches", async () => {
|
|
646
|
-
const client = new AgentClient();
|
|
647
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
648
|
-
Reflect.set(client, "projectId", "demo");
|
|
649
|
-
const calledTools = [];
|
|
650
|
-
Reflect.set(client, "httpClient", {
|
|
651
|
-
callTool: async (toolName) => {
|
|
652
|
-
calledTools.push(toolName);
|
|
653
|
-
if (toolName === "get_dm_messages") {
|
|
654
|
-
return { messages: [] };
|
|
655
|
-
}
|
|
656
|
-
if (toolName === "discover_group_rooms") {
|
|
657
|
-
return { groups: [] };
|
|
658
|
-
}
|
|
659
|
-
return {};
|
|
660
|
-
},
|
|
661
|
-
});
|
|
662
|
-
await client.fetchUnreadMessages();
|
|
663
|
-
assert.equal(calledTools[0], "get_dm_messages");
|
|
664
|
-
assert.equal(calledTools.includes("poll_message"), false);
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
describe("discoverGroupRooms", () => {
|
|
668
|
-
it("should call discover_group_rooms tool", async () => {
|
|
669
|
-
const client = new AgentClient();
|
|
670
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
671
|
-
const expectedResponse = {
|
|
672
|
-
groups: [
|
|
673
|
-
{
|
|
674
|
-
id: "project:demo",
|
|
675
|
-
projectId: "demo",
|
|
676
|
-
roomType: "project",
|
|
677
|
-
scopeRefId: "demo",
|
|
678
|
-
name: "demo group",
|
|
679
|
-
memberUserIds: [],
|
|
680
|
-
},
|
|
681
|
-
],
|
|
682
|
-
};
|
|
683
|
-
Reflect.set(client, "httpClient", {
|
|
684
|
-
callTool: async () => expectedResponse,
|
|
685
|
-
});
|
|
686
|
-
const result = await client.discoverGroupRooms();
|
|
687
|
-
assert.deepEqual(result, expectedResponse);
|
|
688
|
-
});
|
|
689
|
-
it("should throw when not registered", async () => {
|
|
690
|
-
const client = new AgentClient();
|
|
691
|
-
await assert.rejects(async () => await client.discoverGroupRooms(), /Agent is not registered/);
|
|
692
|
-
});
|
|
693
|
-
});
|
|
694
|
-
describe("hireColleague", () => {
|
|
695
|
-
it("should call hire_colleague tool with correct parameters", async () => {
|
|
696
|
-
const client = new AgentClient();
|
|
697
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
698
|
-
const hireArgs = {
|
|
699
|
-
roleName: "backend-developer",
|
|
700
|
-
displayName: "Backend Dev-NEW",
|
|
701
|
-
prompt: "backend expert",
|
|
702
|
-
workspacePath: "/workspace/new",
|
|
703
|
-
};
|
|
704
|
-
const expectedResult = {
|
|
705
|
-
id: "agent-new",
|
|
706
|
-
displayName: "Backend Dev-NEW",
|
|
707
|
-
roleName: "backend-developer",
|
|
708
|
-
projectId: "demo",
|
|
709
|
-
workspacePath: "/workspace/new",
|
|
710
|
-
runtimeBridgeBaseUrl: "http://127.0.0.1:18081",
|
|
711
|
-
runtimeStatus: "stopped",
|
|
712
|
-
};
|
|
713
|
-
let capturedArgs;
|
|
714
|
-
Reflect.set(client, "httpClient", {
|
|
715
|
-
callTool: async (_toolName, args) => {
|
|
716
|
-
capturedArgs = args;
|
|
717
|
-
return expectedResult;
|
|
718
|
-
},
|
|
719
|
-
});
|
|
720
|
-
const result = await client.hireColleague(hireArgs);
|
|
721
|
-
assert.equal(capturedArgs?.agentId, "agent-1");
|
|
722
|
-
assert.equal(capturedArgs?.roleName, "backend-developer");
|
|
723
|
-
assert.deepEqual(result, expectedResult);
|
|
724
|
-
});
|
|
725
|
-
});
|
|
726
|
-
describe("hireColleaguesBatch", () => {
|
|
727
|
-
it("should call hire_colleagues_batch tool with correct parameters", async () => {
|
|
728
|
-
const client = new AgentClient();
|
|
729
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
730
|
-
const batchArgs = {
|
|
731
|
-
projectId: "demo",
|
|
732
|
-
items: [
|
|
733
|
-
{
|
|
734
|
-
roleName: "backend-developer",
|
|
735
|
-
displayName: "Backend Dev-A",
|
|
736
|
-
prompt: "backend expert A",
|
|
737
|
-
workspacePath: "/workspace/backend-a",
|
|
738
|
-
},
|
|
739
|
-
{
|
|
740
|
-
newRoleName: "qa-engineer",
|
|
741
|
-
displayName: "QA Engineer-B",
|
|
742
|
-
prompt: "qa expert B",
|
|
743
|
-
workspacePath: "/workspace/qa-b",
|
|
744
|
-
runtimeMcpEnabled: true,
|
|
745
|
-
},
|
|
746
|
-
],
|
|
747
|
-
};
|
|
748
|
-
const expectedResult = {
|
|
749
|
-
batchId: "batch-1",
|
|
750
|
-
projectId: "demo",
|
|
751
|
-
summary: {
|
|
752
|
-
total: 2,
|
|
753
|
-
approved: 1,
|
|
754
|
-
rejected: 1,
|
|
755
|
-
created: 1,
|
|
756
|
-
failed: 0,
|
|
757
|
-
},
|
|
758
|
-
results: [
|
|
759
|
-
{
|
|
760
|
-
itemId: "item-1",
|
|
761
|
-
decision: "APPROVED",
|
|
762
|
-
status: "CREATED",
|
|
763
|
-
createdAgent: {
|
|
764
|
-
id: "agent-new-1",
|
|
765
|
-
displayName: "Backend Dev-A",
|
|
766
|
-
roleName: "backend-developer",
|
|
767
|
-
projectId: "demo",
|
|
768
|
-
workspacePath: "/workspace/backend-a",
|
|
769
|
-
},
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
itemId: "item-2",
|
|
773
|
-
decision: "REJECTED",
|
|
774
|
-
status: "REJECTED",
|
|
775
|
-
reason: "headcount pending",
|
|
776
|
-
},
|
|
777
|
-
],
|
|
778
|
-
};
|
|
779
|
-
let capturedArgs;
|
|
780
|
-
let capturedToolName;
|
|
781
|
-
Reflect.set(client, "httpClient", {
|
|
782
|
-
callTool: async (toolName, args) => {
|
|
783
|
-
capturedToolName = toolName;
|
|
784
|
-
capturedArgs = args;
|
|
785
|
-
return expectedResult;
|
|
786
|
-
},
|
|
787
|
-
});
|
|
788
|
-
const result = await client.hireColleaguesBatch(batchArgs);
|
|
789
|
-
assert.equal(capturedToolName, "hire_colleagues_batch");
|
|
790
|
-
assert.equal(capturedArgs?.agentId, "agent-1");
|
|
791
|
-
assert.equal(capturedArgs?.projectId, "demo");
|
|
792
|
-
assert.deepEqual(capturedArgs?.items, batchArgs.items);
|
|
793
|
-
assert.deepEqual(result, expectedResult);
|
|
794
|
-
});
|
|
795
|
-
it("should throw when batch hire is called before registration", async () => {
|
|
796
|
-
const client = new AgentClient();
|
|
797
|
-
await assert.rejects(async () => await client.hireColleaguesBatch({
|
|
798
|
-
items: [
|
|
799
|
-
{
|
|
800
|
-
roleName: "backend-developer",
|
|
801
|
-
workspacePath: "/workspace/backend-a",
|
|
802
|
-
},
|
|
803
|
-
],
|
|
804
|
-
}), /Agent is not registered/);
|
|
805
|
-
});
|
|
806
|
-
});
|
|
807
|
-
describe("getProfile", () => {
|
|
808
|
-
it("should return profile with describe", async () => {
|
|
809
|
-
const client = new AgentClient();
|
|
810
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
811
|
-
Reflect.set(client, "agentConfig", { describe: "test describe" });
|
|
812
|
-
const profileInfo = createProfileInfo();
|
|
813
|
-
Reflect.set(client, "httpClient", {
|
|
814
|
-
callTool: async () => profileInfo,
|
|
815
|
-
});
|
|
816
|
-
const result = await client.getProfile();
|
|
817
|
-
assert.equal(result.id, "agent-1");
|
|
818
|
-
assert.equal(result.describe, "test describe");
|
|
819
|
-
});
|
|
820
|
-
});
|
|
821
|
-
describe("getState", () => {
|
|
822
|
-
it("should return current state", () => {
|
|
823
|
-
const client = new AgentClient();
|
|
824
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
825
|
-
Reflect.set(client, "displayName", "Agent One");
|
|
826
|
-
Reflect.set(client, "projectId", "demo");
|
|
827
|
-
Reflect.set(client, "isRunning", false);
|
|
828
|
-
Reflect.set(client, "wsClient", {
|
|
829
|
-
isConnected: () => true,
|
|
830
|
-
});
|
|
831
|
-
const state = client.getState();
|
|
832
|
-
assert.equal(state.agentId, "agent-1");
|
|
833
|
-
assert.equal(state.displayName, "Agent One");
|
|
834
|
-
assert.equal(state.projectId, "demo");
|
|
835
|
-
assert.equal(state.isConnected, true);
|
|
836
|
-
assert.equal(state.isRunning, false);
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
describe("start and stop", () => {
|
|
840
|
-
it("should not start when already running", () => {
|
|
841
|
-
const client = new AgentClient();
|
|
842
|
-
Reflect.set(client, "agentId", "agent-1");
|
|
843
|
-
Reflect.set(client, "isRunning", true);
|
|
844
|
-
client.start(async () => "reply");
|
|
845
|
-
assert.equal(Reflect.get(client, "isRunning"), true);
|
|
846
|
-
});
|
|
847
|
-
it("should throw when not registered on start", () => {
|
|
848
|
-
const client = new AgentClient();
|
|
849
|
-
assert.throws(() => client.start(async () => "reply"), /Agent is not registered/);
|
|
850
|
-
});
|
|
851
|
-
it("should stop message loop", () => {
|
|
852
|
-
const client = new AgentClient();
|
|
853
|
-
Reflect.set(client, "isRunning", true);
|
|
854
|
-
client.stop();
|
|
855
|
-
assert.equal(Reflect.get(client, "isRunning"), false);
|
|
856
|
-
});
|
|
857
|
-
});
|
|
858
|
-
describe("profile caching", () => {
|
|
859
|
-
it("should cache profile when profileHandler not set", () => {
|
|
860
|
-
const client = new AgentClient();
|
|
861
|
-
const cachedProfile = createProfileInfo();
|
|
862
|
-
const handleProfileInfo = Reflect.get(client, "handleProfileInfo");
|
|
863
|
-
handleProfileInfo.call(client, cachedProfile);
|
|
864
|
-
const cached = Reflect.get(client, "cachedProfile");
|
|
865
|
-
assert.deepEqual(cached, cachedProfile);
|
|
866
|
-
});
|
|
867
|
-
it("should invoke profileHandler immediately when set", () => {
|
|
868
|
-
const client = new AgentClient();
|
|
869
|
-
let receivedProfile = null;
|
|
870
|
-
Reflect.set(client, "profileHandler", (profile) => {
|
|
871
|
-
receivedProfile = profile;
|
|
872
|
-
});
|
|
873
|
-
const profile = createProfileInfo();
|
|
874
|
-
const handleProfileInfo = Reflect.get(client, "handleProfileInfo");
|
|
875
|
-
handleProfileInfo.call(client, profile);
|
|
876
|
-
assert.deepEqual(receivedProfile, profile);
|
|
877
|
-
assert.equal(Reflect.get(client, "cachedProfile"), null);
|
|
878
|
-
});
|
|
879
|
-
});
|
|
880
|
-
describe("incoming message filtering", () => {
|
|
881
|
-
it("should filter self-sent direct messages", async () => {
|
|
882
|
-
const client = new AgentClient();
|
|
883
|
-
Reflect.set(client, "agentId", "agent-self");
|
|
884
|
-
let handlerCalled = false;
|
|
885
|
-
Reflect.set(client, "messageHandler", async () => {
|
|
886
|
-
handlerCalled = true;
|
|
887
|
-
return "reply";
|
|
888
|
-
});
|
|
889
|
-
const selfMessage = {
|
|
890
|
-
msgId: "dm-1",
|
|
891
|
-
senderId: "agent-self",
|
|
892
|
-
senderName: "Self",
|
|
893
|
-
content: "self message",
|
|
894
|
-
requireReply: false,
|
|
895
|
-
timestamp: new Date().toISOString(),
|
|
896
|
-
};
|
|
897
|
-
const handleIncomingDm = Reflect.get(client, "handleIncomingDm");
|
|
898
|
-
handleIncomingDm.call(client, selfMessage);
|
|
899
|
-
assert.equal(handlerCalled, false);
|
|
900
|
-
});
|
|
901
|
-
it("should filter self-sent group messages", async () => {
|
|
902
|
-
const client = new AgentClient();
|
|
903
|
-
Reflect.set(client, "agentId", "agent-self");
|
|
904
|
-
let handlerCalled = false;
|
|
905
|
-
Reflect.set(client, "messageHandler", async () => {
|
|
906
|
-
handlerCalled = true;
|
|
907
|
-
return "reply";
|
|
908
|
-
});
|
|
909
|
-
const selfGroupMessage = {
|
|
910
|
-
msgId: 1,
|
|
911
|
-
senderId: "agent-self",
|
|
912
|
-
senderName: "Self",
|
|
913
|
-
content: "self group message",
|
|
914
|
-
timestamp: new Date().toISOString(),
|
|
915
|
-
projectId: "demo",
|
|
916
|
-
};
|
|
917
|
-
const handleIncomingGroup = Reflect.get(client, "handleIncomingGroup");
|
|
918
|
-
handleIncomingGroup.call(client, selfGroupMessage);
|
|
919
|
-
assert.equal(handlerCalled, false);
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
describe("fetchUnreadMessages", () => {
|
|
923
|
-
it("should ignore malformed group message responses instead of returning undefined entries", async () => {
|
|
924
|
-
const client = new AgentClient();
|
|
925
|
-
Reflect.set(client, "agentId", "agent-self");
|
|
926
|
-
Reflect.set(client, "projectId", "demo");
|
|
927
|
-
Reflect.set(client, "agentConfig", {
|
|
928
|
-
roleName: "test-role",
|
|
929
|
-
projectId: "demo",
|
|
930
|
-
workspacePath: "/workspace/demo",
|
|
931
|
-
prompt: "",
|
|
932
|
-
describe: "",
|
|
933
|
-
});
|
|
934
|
-
const validGroupMessage = {
|
|
935
|
-
msgId: 7,
|
|
936
|
-
senderId: "agent-other",
|
|
937
|
-
senderName: "Other",
|
|
938
|
-
content: "hello",
|
|
939
|
-
timestamp: new Date().toISOString(),
|
|
940
|
-
projectId: "demo",
|
|
941
|
-
};
|
|
942
|
-
const groups = {
|
|
943
|
-
groups: [
|
|
944
|
-
{
|
|
945
|
-
id: "project:demo",
|
|
946
|
-
projectId: "demo",
|
|
947
|
-
roomType: "project",
|
|
948
|
-
scopeRefId: "demo",
|
|
949
|
-
name: "demo 公共群聊",
|
|
950
|
-
memberUserIds: ["agent-self"],
|
|
951
|
-
},
|
|
952
|
-
{
|
|
953
|
-
id: "inner_group:g-1",
|
|
954
|
-
projectId: "demo",
|
|
955
|
-
roomType: "inner_group",
|
|
956
|
-
scopeRefId: "g-1",
|
|
957
|
-
name: "小组",
|
|
958
|
-
memberUserIds: ["agent-self"],
|
|
959
|
-
},
|
|
960
|
-
],
|
|
961
|
-
};
|
|
962
|
-
Reflect.set(client, "httpClient", {
|
|
963
|
-
callTool: async (toolName, args) => {
|
|
964
|
-
if (toolName === "get_dm_messages") {
|
|
965
|
-
return { messages: [] };
|
|
966
|
-
}
|
|
967
|
-
if (toolName === "discover_group_rooms") {
|
|
968
|
-
return groups;
|
|
969
|
-
}
|
|
970
|
-
if (toolName === "get_group_messages" && args?.roomId === "project:demo") {
|
|
971
|
-
return { messages: [validGroupMessage] };
|
|
972
|
-
}
|
|
973
|
-
return { error: "transient room failure" };
|
|
974
|
-
},
|
|
975
|
-
});
|
|
976
|
-
const result = await client.fetchUnreadMessages();
|
|
977
|
-
assert.deepEqual(result.directMessages, []);
|
|
978
|
-
assert.deepEqual(result.groupMessages, [validGroupMessage]);
|
|
979
|
-
});
|
|
980
|
-
it("should keep other room messages when one room unread fetch throws", async () => {
|
|
981
|
-
const client = new AgentClient();
|
|
982
|
-
Reflect.set(client, "agentId", "agent-self");
|
|
983
|
-
Reflect.set(client, "projectId", "demo");
|
|
984
|
-
Reflect.set(client, "agentConfig", {
|
|
985
|
-
roleName: "test-role",
|
|
986
|
-
projectId: "demo",
|
|
987
|
-
workspacePath: "/workspace/demo",
|
|
988
|
-
prompt: "",
|
|
989
|
-
describe: "",
|
|
990
|
-
});
|
|
991
|
-
const validGroupMessage = {
|
|
992
|
-
msgId: 8,
|
|
993
|
-
senderId: "agent-other",
|
|
994
|
-
senderName: "Other",
|
|
995
|
-
content: "restored after restart",
|
|
996
|
-
timestamp: new Date().toISOString(),
|
|
997
|
-
projectId: "demo",
|
|
998
|
-
};
|
|
999
|
-
const groups = {
|
|
1000
|
-
groups: [
|
|
1001
|
-
{
|
|
1002
|
-
id: "project:demo",
|
|
1003
|
-
projectId: "demo",
|
|
1004
|
-
roomType: "project",
|
|
1005
|
-
scopeRefId: "demo",
|
|
1006
|
-
name: "demo 公共群聊",
|
|
1007
|
-
memberUserIds: ["agent-self"],
|
|
1008
|
-
},
|
|
1009
|
-
{
|
|
1010
|
-
id: "inner_group:g-1",
|
|
1011
|
-
projectId: "demo",
|
|
1012
|
-
roomType: "inner_group",
|
|
1013
|
-
scopeRefId: "g-1",
|
|
1014
|
-
name: "小组",
|
|
1015
|
-
memberUserIds: ["agent-self"],
|
|
1016
|
-
},
|
|
1017
|
-
],
|
|
1018
|
-
};
|
|
1019
|
-
Reflect.set(client, "httpClient", {
|
|
1020
|
-
callTool: async (toolName, args) => {
|
|
1021
|
-
if (toolName === "get_dm_messages") {
|
|
1022
|
-
return { messages: [] };
|
|
1023
|
-
}
|
|
1024
|
-
if (toolName === "discover_group_rooms") {
|
|
1025
|
-
return groups;
|
|
1026
|
-
}
|
|
1027
|
-
if (toolName === "get_group_messages" && args?.roomId === "project:demo") {
|
|
1028
|
-
return { messages: [validGroupMessage] };
|
|
1029
|
-
}
|
|
1030
|
-
throw new Error("transient room failure");
|
|
1031
|
-
},
|
|
1032
|
-
});
|
|
1033
|
-
// logger 在非 TTY(测试子进程为管道)下有意跳过 console 写,改经 WS sink 观察告警
|
|
1034
|
-
const warnMessages = [];
|
|
1035
|
-
setMcpNodeLogSink((entry) => {
|
|
1036
|
-
if (entry.level === "warn") {
|
|
1037
|
-
warnMessages.push(entry.message);
|
|
1038
|
-
}
|
|
1039
|
-
});
|
|
1040
|
-
warnMessages.length = 0; // 丢弃绑定 sink 时同步回放的历史缓冲日志
|
|
1041
|
-
try {
|
|
1042
|
-
const result = await client.fetchUnreadMessages();
|
|
1043
|
-
assert.deepEqual(result.directMessages, []);
|
|
1044
|
-
assert.deepEqual(result.groupMessages, [validGroupMessage]);
|
|
1045
|
-
assert.equal(warnMessages.length, 1);
|
|
1046
|
-
assert.match(warnMessages[0], /跳过群房间未读补拉失败/);
|
|
1047
|
-
}
|
|
1048
|
-
finally {
|
|
1049
|
-
setMcpNodeLogSink(null);
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
});
|
|
1053
|
-
describe("project name update from profile", () => {
|
|
1054
|
-
it("should update projectId when receiving profile with projectId", () => {
|
|
1055
|
-
const client = new AgentClient();
|
|
1056
|
-
Reflect.set(client, "projectId", null);
|
|
1057
|
-
const profile = createProfileInfo({
|
|
1058
|
-
projectId: "server-assigned-project",
|
|
1059
|
-
});
|
|
1060
|
-
Reflect.set(client, "profileHandler", null);
|
|
1061
|
-
const handleProfileInfo = Reflect.get(client, "handleProfileInfo");
|
|
1062
|
-
handleProfileInfo.call(client, profile);
|
|
1063
|
-
assert.equal(Reflect.get(client, "projectId"), "server-assigned-project");
|
|
1064
|
-
});
|
|
1065
|
-
});
|
|
1066
|
-
});
|