aws-runtime-bridge 1.9.130 → 1.9.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +15 -0
  2. package/dist/adapter/AcodeSdkAdapter.js +5 -5
  3. package/dist/adapter/types.d.ts +9 -0
  4. package/dist/browser/browser-frame-encode.d.ts +16 -0
  5. package/dist/browser/browser-frame-encode.js +2 -0
  6. package/dist/browser/browser-pool.js +1 -1
  7. package/dist/browser/built-in-browser-tools.js +1 -1
  8. package/dist/browser/screencast-differ.d.ts +35 -0
  9. package/dist/browser/screencast-differ.js +2 -0
  10. package/dist/browser/screencast-manager.d.ts +32 -0
  11. package/dist/browser/screencast-manager.js +1 -1
  12. package/dist/browser/types.d.ts +48 -1
  13. package/dist/browser/types.js +1 -1
  14. package/dist/browser/ws-server.d.ts +1 -1
  15. package/dist/browser/ws-server.js +1 -1
  16. package/dist/routes/background-tasks.js +7 -1
  17. package/dist/routes/instance-tool-status.d.ts +4 -0
  18. package/dist/routes/instance-tool-status.js +2 -0
  19. package/dist/routes/instance.d.ts +0 -2
  20. package/dist/routes/instance.js +1 -1
  21. package/dist/routes/system-metrics.js +1 -1
  22. package/dist/routes/terminal.js +19 -19
  23. package/dist/services/acode-bg-manager.d.ts +36 -0
  24. package/dist/services/acode-bg-manager.js +2 -0
  25. package/dist/services/browser-session-manager.js +1 -1
  26. package/dist/services/instance-features.d.ts +5 -0
  27. package/dist/services/instance-features.js +1 -1
  28. package/dist/services/process-detail.d.ts +77 -0
  29. package/dist/services/process-detail.js +32 -0
  30. package/dist/services/process-top.d.ts +53 -0
  31. package/dist/services/process-top.js +50 -0
  32. package/dist/services/session-output.d.ts +10 -0
  33. package/dist/services/session-output.js +3 -3
  34. package/package/acode/dist/await-complete-tool.js +4 -4
  35. package/package/acode/dist/background-command-manager.d.ts +39 -0
  36. package/package/acode/dist/background-command-manager.js +5 -5
  37. package/package/acode/dist/background-command-tools.js +1 -1
  38. package/package/acode/dist/built-in-file-tools.d.ts +25 -1
  39. package/package/acode/dist/built-in-file-tools.js +2 -2
  40. package/package/acode/dist/builtins/shellCommandResult.d.ts +3 -0
  41. package/package/acode/dist/builtins/shellCommandResult.js +1 -1
  42. package/package/acode/dist/builtins/shellCommandRunner.d.ts +2 -0
  43. package/package/acode/dist/builtins/shellCommandRunner.js +1 -1
  44. package/package/acode/dist/image-utils.d.ts +52 -0
  45. package/package/acode/dist/image-utils.js +2 -0
  46. package/package/acode/dist/index.d.ts +1 -0
  47. package/package/acode/dist/index.js +1 -1
  48. package/package/acode/dist/runtime.d.ts +15 -1
  49. package/package/acode/dist/runtime.js +26 -26
  50. package/package/acode/dist/sub-agent-tools.js +30 -26
  51. package/package/acode/dist/sub-agent-types.d.ts +1 -1
  52. package/package/acode/dist/tool-allocation.d.ts +4 -4
  53. package/package/acode/dist/tool-allocation.js +1 -1
  54. package/package/acode/dist/types.d.ts +3 -1
  55. package/package/aws-client-agent-mcp/dist/http-client.d.ts +4 -0
  56. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  57. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +8 -0
  58. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -25
  59. package/package/aws-client-agent-mcp/dist/mcp-tools.js +6 -6
  60. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +6 -0
  61. package/package/aws-client-agent-mcp/dist/memory-tools.js +2 -2
  62. package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
  63. package/package/aws-client-agent-mcp/package.json +1 -1
  64. package/package.json +3 -3
  65. package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
  66. package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1066
  67. package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
  68. package/package/aws-client-agent-mcp/dist/config.test.js +0 -259
  69. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -2
  70. package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -312
  71. package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
  72. package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -808
  73. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +0 -1
  74. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +0 -110
  75. package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
  76. package/package/aws-client-agent-mcp/dist/logger.test.js +0 -27
  77. package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
  78. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -2798
  79. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
  80. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -152
  81. package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
  82. package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -664
  83. package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
  84. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -142
  85. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +0 -1
  86. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +0 -81
  87. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
  88. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -201
  89. package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
  90. package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -157
  91. package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +0 -1
  92. package/package/aws-client-agent-mcp/dist/watchdog.test.js +0 -124
  93. package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
  94. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +0 -725
@@ -1,2798 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { after, before, describe, it } from "node:test";
6
- import { AgentClient, PollLongPollTimeoutError } from "./agent-client.js";
7
- import { McpServer } from "./mcp-server.js";
8
- import { MCP_TOOLS } from "./mcp-tools.js";
9
- import { MemoryStore, } from "./memory-store.js";
10
- import { MessageBuffer } from "./message-buffer.js";
11
- function createServerWithUnread(unreadMessages, overrides = {}) {
12
- const server = new McpServer();
13
- Reflect.set(server, "messageBuffer", new MessageBuffer());
14
- const agentClientStub = {
15
- getStatusReporter: () => null,
16
- getMyTasks: async () => [],
17
- getState: () => ({
18
- agentId: "agent-self",
19
- displayName: "Self Agent",
20
- isConnected: true,
21
- isRegistered: true,
22
- }),
23
- fetchUnreadMessages: async () => unreadMessages,
24
- markMeetingRead: async () => { },
25
- callAgentScopedTool: async () => {
26
- throw new Error("unexpected callAgentScopedTool in test stub");
27
- },
28
- ...overrides,
29
- };
30
- Reflect.set(server, "agentClient", agentClientStub);
31
- return server;
32
- }
33
- async function withMemoryServer(callback) {
34
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-"));
35
- const server = new McpServer();
36
- Reflect.set(server, "memoryStore", new MemoryStore(path.join(dir, "instance-memory")));
37
- Reflect.set(server, "roleMemoryStore", new MemoryStore(path.join(dir, "role-memory")));
38
- try {
39
- return await callback(server);
40
- }
41
- finally {
42
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
43
- }
44
- }
45
- async function withScopedMemoryServers(callback) {
46
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-scoped-memory-"));
47
- const firstServer = new McpServer();
48
- const secondServer = new McpServer();
49
- Reflect.set(firstServer, "memoryStore", new MemoryStore(path.join(dir, "agent-one-memory")));
50
- Reflect.set(secondServer, "memoryStore", new MemoryStore(path.join(dir, "agent-two-memory")));
51
- Reflect.set(firstServer, "roleMemoryStore", new MemoryStore(path.join(dir, "shared-role-memory")));
52
- Reflect.set(secondServer, "roleMemoryStore", new MemoryStore(path.join(dir, "shared-role-memory")));
53
- try {
54
- return await callback(firstServer, secondServer);
55
- }
56
- finally {
57
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
58
- }
59
- }
60
- function getHandleToolCall(server) {
61
- return Reflect.get(server, "handleToolCall");
62
- }
63
- /**
64
- * 测试辅助:访问 McpServer 的私有 messageBuffer(WebSocket 本地消息队列),
65
- * 用于模拟外部消息到达后唤醒阻塞中的 poll_message。
66
- * 注意:直接依赖私有字段名,若 mcp-server.ts 重命名该字段需同步修改本函数,
67
- * 避免测试在字段改名后静默失效(Reflect 访问不会报编译错误)。
68
- */
69
- function getMessageBuffer(server) {
70
- return Reflect.get(server, "messageBuffer");
71
- }
72
- function attachMemoryStatsReporter(server, reportedStats) {
73
- const existingClient = Reflect.get(server, "agentClient");
74
- Reflect.set(server, "agentClient", {
75
- ...existingClient,
76
- getStatusReporter: () => null,
77
- reportMemoryStats: async (memoryStats) => {
78
- reportedStats.push(memoryStats);
79
- },
80
- });
81
- }
82
- function withTemporaryEnv(values, callback) {
83
- const previousValues = new Map();
84
- for (const [key, value] of Object.entries(values)) {
85
- previousValues.set(key, process.env[key]);
86
- if (value === undefined) {
87
- delete process.env[key];
88
- }
89
- else {
90
- process.env[key] = value;
91
- }
92
- }
93
- try {
94
- return callback();
95
- }
96
- finally {
97
- for (const [key, value] of previousValues.entries()) {
98
- if (value === undefined) {
99
- delete process.env[key];
100
- }
101
- else {
102
- process.env[key] = value;
103
- }
104
- }
105
- }
106
- }
107
- describe("MCP task tool exposure", () => {
108
- it("should not expose start_task because task claiming is silent", () => {
109
- assert.equal(MCP_TOOLS.some((tool) => tool.name === "start_task"), false);
110
- });
111
- });
112
- describe("McpServer unread backlog hydration", () => {
113
- it("poll_message should return unread direct and group messages fetched after restart", async () => {
114
- const directMessage = {
115
- msgId: "dm-offline-1",
116
- senderId: "agent-b",
117
- senderName: "Agent B",
118
- content: "offline dm",
119
- requireReply: false,
120
- timestamp: new Date().toISOString(),
121
- };
122
- const groupMessage = {
123
- msgId: 101,
124
- senderId: "agent-c",
125
- senderName: "Agent C",
126
- content: "offline group",
127
- timestamp: new Date().toISOString(),
128
- projectId: "demo",
129
- };
130
- const server = createServerWithUnread({
131
- directMessages: [directMessage],
132
- groupMessages: [groupMessage],
133
- });
134
- const handleToolCall = Reflect.get(server, "handleToolCall");
135
- const result = await handleToolCall.call(server, "poll_message", {});
136
- assert.deepEqual(result, {
137
- directMessages: [directMessage],
138
- groupMessages: [groupMessage],
139
- fromBuffer: true,
140
- systemMessages: [],
141
- });
142
- });
143
- it("get_message should return unread messages fetched when local buffer is empty", async () => {
144
- const directMessage = {
145
- msgId: "dm-offline-2",
146
- senderId: "agent-d",
147
- senderName: "Agent D",
148
- content: "wake up dm",
149
- requireReply: false,
150
- timestamp: new Date().toISOString(),
151
- };
152
- const server = createServerWithUnread({
153
- directMessages: [directMessage],
154
- groupMessages: [],
155
- });
156
- const handleToolCall = Reflect.get(server, "handleToolCall");
157
- const result = await handleToolCall.call(server, "get_message", {});
158
- assert.deepEqual(result, {
159
- directMessages: [directMessage],
160
- groupMessages: [],
161
- fromBuffer: false,
162
- systemMessages: [],
163
- });
164
- });
165
- it("poll_message should keep blocking through empty polls until messages arrive", async () => {
166
- const directMessage = {
167
- msgId: "dm-delayed-1",
168
- senderId: "agent-delayed",
169
- senderName: "Agent Delayed",
170
- content: "delayed dm",
171
- requireReply: false,
172
- timestamp: new Date().toISOString(),
173
- };
174
- const fetchOptions = [];
175
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
176
- fetchUnreadMessages: async (options) => {
177
- fetchOptions.push(options ?? {});
178
- return { directMessages: [], groupMessages: [] };
179
- },
180
- });
181
- // Simulate a WebSocket message arriving after hydration returns empty.
182
- // poll_message must keep blocking on the local buffer until the message arrives.
183
- const buffer = getMessageBuffer(server);
184
- setTimeout(() => {
185
- buffer.push(directMessage);
186
- }, 50);
187
- const handleToolCall = getHandleToolCall(server);
188
- const result = await handleToolCall.call(server, "poll_message", {});
189
- assert.deepEqual(result, {
190
- directMessages: [directMessage],
191
- groupMessages: [],
192
- fromBuffer: true,
193
- systemMessages: [],
194
- });
195
- // fetchUnreadMessages should only be called once (the initial non-blocking hydrate).
196
- // The message must arrive via the local buffer (WebSocket push), not a second blocking fetch.
197
- assert.equal(fetchOptions.length, 1);
198
- assert.equal(fetchOptions[0].blockIfEmpty, false);
199
- assert.equal(fetchOptions[0].includePipelineTasks, true);
200
- });
201
- it("poll_message should exit promptly when the client aborts the request", async () => {
202
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
203
- fetchUnreadMessages: async () => ({ directMessages: [], groupMessages: [] }),
204
- });
205
- const controller = new AbortController();
206
- const handleToolCall = getHandleToolCall(server);
207
- const pending = handleToolCall.call(server, "poll_message", {}, controller.signal);
208
- // 等循环进入 messageBuffer.poll 阻塞,再模拟客户端取消(MCP 超时/断开触发 extra.signal)
209
- await new Promise((resolve) => setTimeout(resolve, 50));
210
- controller.abort();
211
- getMessageBuffer(server).wakeUpWaiters();
212
- const result = (await Promise.race([
213
- pending,
214
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
215
- ]));
216
- assert.notEqual(result, "timeout", "poll_message must exit after client abort instead of blocking forever");
217
- assert.deepEqual(result.directMessages, []);
218
- assert.deepEqual(result.groupMessages, []);
219
- });
220
- it("poll_message must requeue consumed messages when aborted after consumption", async () => {
221
- // 消息打服务端标记:让阻塞循环在消费后进入 mark_dm_read 撤回确认,
222
- // 通过慢确认把 abort 卡在"已消费、未交付"窗口内(真实 race 场景的时间线)
223
- const directMessage = {
224
- msgId: "dm-abort-after-consume",
225
- senderId: "agent-sender",
226
- senderName: "Agent Sender",
227
- content: "message consumed right before abort",
228
- requireReply: false,
229
- timestamp: new Date().toISOString(),
230
- _awsServerMessage: true,
231
- };
232
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
233
- fetchUnreadMessages: async () => ({ directMessages: [], groupMessages: [] }),
234
- markDmRead: async (msgIds) => {
235
- await new Promise((resolve) => setTimeout(resolve, 150));
236
- return new Set(msgIds);
237
- },
238
- });
239
- const controller = new AbortController();
240
- const handleToolCall = getHandleToolCall(server);
241
- const buffer = getMessageBuffer(server);
242
- const pending = handleToolCall.call(server, "poll_message", {}, controller.signal);
243
- // 消息在 poll 阻塞期间到达并被消费(卡在 mark_dm_read 确认中),随后调用方取消
244
- await new Promise((resolve) => setTimeout(resolve, 50));
245
- buffer.push(directMessage);
246
- await new Promise((resolve) => setTimeout(resolve, 20));
247
- assert.ok(controller.signal.aborted === false);
248
- controller.abort();
249
- getMessageBuffer(server).wakeUpWaiters();
250
- await Promise.race([
251
- pending,
252
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
253
- ]);
254
- // 关键断言:被取消的 poll 消费过的消息必须回滚到缓冲区,
255
- // 否则下次 poll_message 永远拿不到(本地去重 + 服务端已读双重拦截)
256
- assert.equal(buffer.size(), 1, "consumed message must be requeued back to the buffer after abort");
257
- const redelivered = await buffer.poll(0);
258
- assert.deepEqual(redelivered.directMessages.map((m) => m.msgId), [directMessage.msgId]);
259
- });
260
- it("poll_message should handle hydration error gracefully and receive WebSocket messages", async () => {
261
- const directMessage = {
262
- msgId: "dm-hydration-error-ws",
263
- senderId: "agent-ws",
264
- senderName: "Agent WS",
265
- content: "delivered via WebSocket after hydration error",
266
- requireReply: false,
267
- timestamp: new Date().toISOString(),
268
- };
269
- let fetchCount = 0;
270
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
271
- fetchUnreadMessages: async () => {
272
- fetchCount += 1;
273
- throw new Error("MCP tool poll_message timed out after 120000ms");
274
- },
275
- });
276
- // Simulate WebSocket message arriving after hydration fails
277
- const buffer = getMessageBuffer(server);
278
- setTimeout(() => {
279
- buffer.push(directMessage);
280
- }, 50);
281
- const handleToolCall = getHandleToolCall(server);
282
- const result = await handleToolCall.call(server, "poll_message", {});
283
- assert.deepEqual(result, {
284
- directMessages: [directMessage],
285
- groupMessages: [],
286
- fromBuffer: true,
287
- systemMessages: [],
288
- });
289
- // fetchUnreadMessages should only be called once (the initial hydrate, which throws).
290
- // hydrateUnreadMessagesFromServer catches the error, and poll_message falls back to
291
- // blocking on the local buffer until the WebSocket message arrives.
292
- assert.equal(fetchCount, 1);
293
- });
294
- it("get_message should still hydrate unread group messages after profile push is received", async () => {
295
- const groupMessage = {
296
- msgId: 301,
297
- senderId: "agent-e",
298
- senderName: "Agent E",
299
- content: "offline group after profile",
300
- timestamp: new Date().toISOString(),
301
- projectId: "demo",
302
- roomId: "inner_group:g-1",
303
- };
304
- const server = createServerWithUnread({
305
- directMessages: [],
306
- groupMessages: [groupMessage],
307
- });
308
- const messageBuffer = getMessageBuffer(server);
309
- messageBuffer.setProfileMessage({
310
- id: "agent-1",
311
- displayName: "Agent One",
312
- roleName: "Backend Agent",
313
- prompt: "prompt",
314
- isOnline: true,
315
- onlineSince: new Date().toISOString(),
316
- createdAt: new Date().toISOString(),
317
- projectId: "demo",
318
- });
319
- const handleToolCall = Reflect.get(server, "handleToolCall");
320
- const result = (await handleToolCall.call(server, "get_message", {}));
321
- assert.equal(result.fromBuffer, false);
322
- assert.deepEqual(result.directMessages, []);
323
- assert.deepEqual(result.groupMessages, [groupMessage]);
324
- });
325
- it("get_message should filter out self-sent unread group messages fetched from server", async () => {
326
- const selfGroupMessage = {
327
- msgId: 401,
328
- senderId: "agent-self",
329
- senderName: "Self Agent",
330
- content: "my own group message",
331
- timestamp: new Date().toISOString(),
332
- projectId: "demo",
333
- roomId: "project:demo",
334
- };
335
- const otherGroupMessage = {
336
- msgId: 402,
337
- senderId: "agent-other",
338
- senderName: "Other Agent",
339
- content: "other group message",
340
- timestamp: new Date().toISOString(),
341
- projectId: "demo",
342
- roomId: "project:demo",
343
- };
344
- const server = createServerWithUnread({
345
- directMessages: [],
346
- groupMessages: [selfGroupMessage, otherGroupMessage],
347
- });
348
- Reflect.set(server, "agentClient", {
349
- fetchUnreadMessages: async () => ({
350
- directMessages: [],
351
- groupMessages: [selfGroupMessage, otherGroupMessage],
352
- }),
353
- getState: () => ({
354
- agentId: "agent-self",
355
- displayName: "Self Agent",
356
- isConnected: true,
357
- isRegistered: true,
358
- }),
359
- getMyTasks: async () => [],
360
- getStatusReporter: () => null,
361
- });
362
- const handleToolCall = Reflect.get(server, "handleToolCall");
363
- const result = await handleToolCall.call(server, "get_message", {});
364
- assert.deepEqual(result, {
365
- directMessages: [],
366
- groupMessages: [otherGroupMessage],
367
- fromBuffer: false,
368
- systemMessages: [],
369
- });
370
- });
371
- it("poll_message should filter out self-sent unread messages fetched from server", async () => {
372
- const selfDirectMessage = {
373
- msgId: "dm-self-1",
374
- senderId: "agent-self",
375
- senderName: "Self Agent",
376
- receiverId: "agent-self",
377
- content: "my own direct message",
378
- requireReply: false,
379
- timestamp: new Date().toISOString(),
380
- };
381
- const otherDirectMessage = {
382
- msgId: "dm-other-1",
383
- senderId: "agent-other",
384
- senderName: "Other Agent",
385
- receiverId: "agent-self",
386
- content: "other direct message",
387
- requireReply: false,
388
- timestamp: new Date().toISOString(),
389
- };
390
- const selfGroupMessage = {
391
- msgId: 403,
392
- senderId: "agent-self",
393
- senderName: "Self Agent",
394
- content: "my own group message",
395
- timestamp: new Date().toISOString(),
396
- projectId: "demo",
397
- roomId: "project:demo",
398
- };
399
- const otherGroupMessage = {
400
- msgId: 404,
401
- senderId: "agent-other",
402
- senderName: "Other Agent",
403
- content: "other group message",
404
- timestamp: new Date().toISOString(),
405
- projectId: "demo",
406
- roomId: "project:demo",
407
- };
408
- const server = createServerWithUnread({
409
- directMessages: [selfDirectMessage, otherDirectMessage],
410
- groupMessages: [selfGroupMessage, otherGroupMessage],
411
- });
412
- const handleToolCall = getHandleToolCall(server);
413
- const result = (await handleToolCall.call(server, "poll_message", {}));
414
- assert.deepEqual(result.directMessages, [otherDirectMessage]);
415
- assert.deepEqual(result.groupMessages, [otherGroupMessage]);
416
- assert.equal(result.fromBuffer, true);
417
- });
418
- it("get_message should ignore malformed unread group entries instead of crashing", async () => {
419
- const validGroupMessage = {
420
- msgId: 501,
421
- senderId: "agent-other",
422
- senderName: "Other Agent",
423
- content: "valid group message",
424
- timestamp: new Date().toISOString(),
425
- projectId: "demo",
426
- roomId: "project:demo",
427
- };
428
- const server = createServerWithUnread({
429
- directMessages: [],
430
- groupMessages: [validGroupMessage],
431
- });
432
- Reflect.set(server, "agentClient", {
433
- fetchUnreadMessages: async () => ({
434
- directMessages: [],
435
- groupMessages: [undefined, validGroupMessage],
436
- }),
437
- getState: () => ({
438
- agentId: "agent-self",
439
- displayName: "Self Agent",
440
- isConnected: true,
441
- isRegistered: true,
442
- }),
443
- getMyTasks: async () => [],
444
- getStatusReporter: () => null,
445
- });
446
- const handleToolCall = getHandleToolCall(server);
447
- const result = await handleToolCall.call(server, "get_message", {});
448
- assert.deepEqual(result, {
449
- directMessages: [],
450
- groupMessages: [validGroupMessage],
451
- fromBuffer: false,
452
- systemMessages: [],
453
- });
454
- });
455
- it("get_message should pass through backend task notification without local my_task injection", async () => {
456
- const taskNotification = {
457
- msgId: "task-notify-backend-1",
458
- senderId: "MCP-System",
459
- senderName: "系统",
460
- receiverId: "agent-self",
461
- content: "你有任务需要关注!1 个待处理任务:实现接口。",
462
- requireReply: false,
463
- prompt: "后端动态任务提醒",
464
- timestamp: new Date().toISOString(),
465
- };
466
- let myTaskCalls = 0;
467
- const server = createServerWithUnread({
468
- directMessages: [taskNotification],
469
- groupMessages: [],
470
- }, {
471
- getMyTasks: async () => {
472
- myTaskCalls += 1;
473
- return [];
474
- },
475
- });
476
- const handleToolCall = getHandleToolCall(server);
477
- const result = (await handleToolCall.call(server, "get_message", {}));
478
- assert.deepEqual(result.directMessages, [taskNotification]);
479
- assert.deepEqual(result.groupMessages, []);
480
- assert.equal(result.fromBuffer, false);
481
- assert.equal(myTaskCalls, 0);
482
- });
483
- it("poll_message should attempt non-blocking backend task claim before local return", async () => {
484
- const taskNotification = {
485
- msgId: "task-notify-backend-2",
486
- senderId: "MCP-System",
487
- senderName: "系统",
488
- receiverId: "agent-self",
489
- content: "你有任务需要关注!1 个处理中任务:需要提交。",
490
- requireReply: false,
491
- prompt: "后端动态任务提醒",
492
- timestamp: new Date().toISOString(),
493
- };
494
- const fetchOptions = [];
495
- const server = createServerWithUnread({
496
- directMessages: [],
497
- groupMessages: [],
498
- }, {
499
- fetchUnreadMessages: async (options) => {
500
- fetchOptions.push(options);
501
- return { directMessages: [taskNotification], groupMessages: [] };
502
- },
503
- });
504
- const handleToolCall = getHandleToolCall(server);
505
- const result = (await handleToolCall.call(server, "poll_message", {}));
506
- assert.deepEqual(result.directMessages, [taskNotification]);
507
- assert.deepEqual(result.groupMessages, []);
508
- assert.equal(result.fromBuffer, true);
509
- assert.equal(fetchOptions.length, 1);
510
- assert.equal(fetchOptions[0]?.blockIfEmpty, false);
511
- assert.equal(fetchOptions[0]?.includePipelineTasks, true);
512
- assert.equal(fetchOptions[0]?.blockTimeoutMs, undefined);
513
- });
514
- it("poll_message should merge existing local cache with newly claimed task notification", async () => {
515
- const cachedMessage = {
516
- msgId: "cached-dm-before-task",
517
- senderId: "agent-cached",
518
- senderName: "Cached Agent",
519
- receiverId: "agent-self",
520
- content: "cached message already in local buffer",
521
- requireReply: false,
522
- timestamp: new Date().toISOString(),
523
- };
524
- const taskNotification = {
525
- msgId: "task-notify-claimed-with-cache",
526
- senderId: "MCP-System",
527
- senderName: "系统",
528
- receiverId: "agent-self",
529
- content: "你有任务需要关注!已通过 poll_message 静默抢占。",
530
- requireReply: false,
531
- prompt: "后端动态任务提醒",
532
- timestamp: new Date().toISOString(),
533
- };
534
- const fetchOptions = [];
535
- const server = createServerWithUnread({
536
- directMessages: [],
537
- groupMessages: [],
538
- }, {
539
- fetchUnreadMessages: async (options) => {
540
- fetchOptions.push(options);
541
- return { directMessages: [taskNotification], groupMessages: [] };
542
- },
543
- });
544
- const messageBuffer = getMessageBuffer(server);
545
- messageBuffer.push(cachedMessage);
546
- const handleToolCall = getHandleToolCall(server);
547
- const result = (await handleToolCall.call(server, "poll_message", {}));
548
- assert.deepEqual(result.directMessages, [cachedMessage, taskNotification]);
549
- assert.deepEqual(result.groupMessages, []);
550
- assert.equal(result.fromBuffer, true);
551
- assert.equal(fetchOptions.length, 1);
552
- assert.equal(fetchOptions[0]?.blockIfEmpty, false);
553
- assert.equal(fetchOptions[0]?.includePipelineTasks, true);
554
- });
555
- it("poll_message should wait for WebSocket messages when backend long polling returns empty", async () => {
556
- const delayedMessage = {
557
- msgId: "dm-after-empty-poll",
558
- senderId: "agent-later",
559
- senderName: "Later Agent",
560
- receiverId: "agent-self",
561
- content: "arrived after empty backend poll",
562
- requireReply: false,
563
- timestamp: new Date().toISOString(),
564
- };
565
- let fetchCalls = 0;
566
- const server = createServerWithUnread({
567
- directMessages: [],
568
- groupMessages: [],
569
- }, {
570
- fetchUnreadMessages: async () => {
571
- fetchCalls += 1;
572
- return { directMessages: [], groupMessages: [] };
573
- },
574
- });
575
- // Simulate WebSocket message arriving after backend returns empty
576
- const buffer = getMessageBuffer(server);
577
- setTimeout(() => {
578
- buffer.push(delayedMessage);
579
- }, 50);
580
- const handleToolCall = getHandleToolCall(server);
581
- const result = (await handleToolCall.call(server, "poll_message", {}));
582
- // Only the initial non-blocking hydrate should call fetchUnreadMessages.
583
- // The message must arrive via the local buffer (WebSocket push).
584
- assert.equal(fetchCalls, 1);
585
- assert.deepEqual(result.directMessages, [delayedMessage]);
586
- assert.deepEqual(result.groupMessages, []);
587
- assert.equal(result.fromBuffer, true);
588
- });
589
- it("poll_message should restore waiting status when task notification is returned", async () => {
590
- const statusEvents = [];
591
- const server = createServerWithUnread({
592
- directMessages: [],
593
- groupMessages: [],
594
- }, {
595
- fetchUnreadMessages: async () => ({
596
- directMessages: [
597
- {
598
- msgId: "task-notify-thinking",
599
- senderId: "MCP-System",
600
- senderName: "系统",
601
- receiverId: "agent-self",
602
- content: "你有任务需要关注!处理待办流程。",
603
- requireReply: false,
604
- timestamp: new Date().toISOString(),
605
- },
606
- ],
607
- groupMessages: [],
608
- }),
609
- getStatusReporter: () => ({
610
- reportToolUse: () => statusEvents.push("tool_using"),
611
- reportToolResult: () => statusEvents.push("tool_result"),
612
- reportThinking: () => statusEvents.push("thinking"),
613
- reportWaitingInput: () => statusEvents.push("waiting_input"),
614
- reportError: () => statusEvents.push("error"),
615
- reportToolError: () => statusEvents.push("tool_error"),
616
- }),
617
- });
618
- const handleToolCall = getHandleToolCall(server);
619
- const result = (await handleToolCall.call(server, "poll_message", {}));
620
- assert.equal(result.directMessages.length, 1);
621
- assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
622
- });
623
- it("get_message should restore thinking status when task notification is returned", async () => {
624
- const statusEvents = [];
625
- const server = createServerWithUnread({
626
- directMessages: [],
627
- groupMessages: [],
628
- }, {
629
- fetchUnreadMessages: async () => ({
630
- directMessages: [
631
- {
632
- msgId: "task-notify-get-message",
633
- senderId: "MCP-System",
634
- senderName: "系统",
635
- receiverId: "agent-self",
636
- content: "你有任务需要关注!继续流程任务。",
637
- requireReply: false,
638
- timestamp: new Date().toISOString(),
639
- },
640
- ],
641
- groupMessages: [],
642
- }),
643
- getStatusReporter: () => ({
644
- reportToolUse: () => statusEvents.push("tool_using"),
645
- reportToolResult: () => statusEvents.push("tool_result"),
646
- reportThinking: () => statusEvents.push("thinking"),
647
- reportWaitingInput: () => statusEvents.push("waiting_input"),
648
- reportError: () => statusEvents.push("error"),
649
- reportToolError: () => statusEvents.push("tool_error"),
650
- }),
651
- });
652
- const handleToolCall = getHandleToolCall(server);
653
- const result = (await handleToolCall.call(server, "get_message", {}));
654
- assert.equal(result.directMessages.length, 1);
655
- assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
656
- });
657
- it("poll_message should restore thinking status when unread messages are returned", async () => {
658
- const statusEvents = [];
659
- const directMessage = {
660
- msgId: "dm-status-thinking",
661
- senderId: "agent-status-source",
662
- senderName: "Status Source",
663
- content: "work message",
664
- requireReply: false,
665
- timestamp: new Date().toISOString(),
666
- };
667
- const server = createServerWithUnread({
668
- directMessages: [directMessage],
669
- groupMessages: [],
670
- }, {
671
- getStatusReporter: () => ({
672
- reportToolUse: () => statusEvents.push("tool_using"),
673
- reportToolResult: () => statusEvents.push("tool_result"),
674
- reportThinking: () => statusEvents.push("thinking"),
675
- reportWaitingInput: () => statusEvents.push("waiting_input"),
676
- reportError: () => statusEvents.push("error"),
677
- reportToolError: () => statusEvents.push("tool_error"),
678
- }),
679
- });
680
- const handleToolCall = getHandleToolCall(server);
681
- const result = (await handleToolCall.call(server, "poll_message", {}));
682
- assert.deepEqual(result.directMessages, [directMessage]);
683
- assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
684
- });
685
- it("get_message should restore thinking status when unread messages are returned", async () => {
686
- const statusEvents = [];
687
- const directMessage = {
688
- msgId: "dm-get-status-waiting",
689
- senderId: "agent-get-status-source",
690
- senderName: "Get Status Source",
691
- content: "private work message",
692
- requireReply: false,
693
- timestamp: new Date().toISOString(),
694
- };
695
- const server = createServerWithUnread({
696
- directMessages: [directMessage],
697
- groupMessages: [],
698
- }, {
699
- getStatusReporter: () => ({
700
- reportToolUse: () => statusEvents.push("tool_using"),
701
- reportToolResult: () => statusEvents.push("tool_result"),
702
- reportThinking: () => statusEvents.push("thinking"),
703
- reportWaitingInput: () => statusEvents.push("waiting_input"),
704
- reportError: () => statusEvents.push("error"),
705
- reportToolError: () => statusEvents.push("tool_error"),
706
- }),
707
- });
708
- const handleToolCall = getHandleToolCall(server);
709
- const result = (await handleToolCall.call(server, "get_message", {}));
710
- assert.deepEqual(result.directMessages, [directMessage]);
711
- assert.deepEqual(statusEvents, [
712
- "tool_using",
713
- "tool_result",
714
- "thinking",
715
- ]);
716
- });
717
- it("get_message should restore thinking status when no messages or tasks are returned", async () => {
718
- const statusEvents = [];
719
- const server = createServerWithUnread({
720
- directMessages: [],
721
- groupMessages: [],
722
- }, {
723
- getStatusReporter: () => ({
724
- reportToolUse: () => statusEvents.push("tool_using"),
725
- reportToolResult: () => statusEvents.push("tool_result"),
726
- reportThinking: () => statusEvents.push("thinking"),
727
- reportWaitingInput: () => statusEvents.push("waiting_input"),
728
- reportError: () => statusEvents.push("error"),
729
- reportToolError: () => statusEvents.push("tool_error"),
730
- }),
731
- });
732
- const handleToolCall = getHandleToolCall(server);
733
- const result = (await handleToolCall.call(server, "get_message", {}));
734
- assert.deepEqual(result.directMessages, []);
735
- assert.deepEqual(result.groupMessages, []);
736
- assert.deepEqual(statusEvents, [
737
- "tool_using",
738
- "tool_result",
739
- "thinking",
740
- ]);
741
- });
742
- it("poll_message should skip reportToolResult and reportThinking when only _acodeAutoDelegate messages are returned", async () => {
743
- const statusEvents = [];
744
- const reportedResults = [];
745
- const delegateMessage = {
746
- msgId: "memory-update-pending-1",
747
- senderId: "MCP-System",
748
- senderName: "系统",
749
- receiverId: "agent-self",
750
- content: "您有以下 Memory 正处于更新中状态",
751
- requireReply: false,
752
- timestamp: new Date().toISOString(),
753
- _acodeAutoDelegate: {
754
- type: "memory_file_change",
755
- uris: ["architecture://relay/core-routing"],
756
- },
757
- };
758
- const server = createServerWithUnread({
759
- directMessages: [delegateMessage],
760
- groupMessages: [],
761
- }, {
762
- getStatusReporter: () => ({
763
- reportToolUse: () => statusEvents.push("tool_using"),
764
- reportToolResult: (r) => {
765
- statusEvents.push("tool_result");
766
- reportedResults.push(r);
767
- },
768
- reportThinking: () => statusEvents.push("thinking"),
769
- reportWaitingInput: () => statusEvents.push("waiting_input"),
770
- reportError: () => statusEvents.push("error"),
771
- reportToolError: () => statusEvents.push("tool_error"),
772
- }),
773
- });
774
- const handleToolCall = getHandleToolCall(server);
775
- const result = (await handleToolCall.call(server, "poll_message", {}));
776
- assert.equal(result.directMessages.length, 1);
777
- // 委托消息:保持 tool_using 状态,不上报 tool_result 也不上报 thinking
778
- // 主 agent 仍在 poll_message 循环中等待真正的消息,委托给子 agent 处理不应影响主 agent 状态
779
- assert.deepEqual(statusEvents, ["tool_using"]);
780
- assert.equal(reportedResults.length, 0);
781
- });
782
- it("poll_message should filter _acodeAutoDelegate from reportToolResult but keep normal messages", async () => {
783
- const statusEvents = [];
784
- const reportedResults = [];
785
- const delegateMessage = {
786
- msgId: "memory-update-pending-2",
787
- senderId: "MCP-System",
788
- senderName: "系统",
789
- receiverId: "agent-self",
790
- content: "请继续完成更新工作",
791
- requireReply: false,
792
- timestamp: new Date().toISOString(),
793
- _acodeAutoDelegate: {
794
- type: "memory_file_change",
795
- uris: ["architecture://overview/system-architecture"],
796
- },
797
- };
798
- const normalMessage = {
799
- msgId: "dm-normal-work",
800
- senderId: "agent-colleague",
801
- senderName: "Colleague",
802
- content: "请帮忙看一下这个接口",
803
- requireReply: false,
804
- timestamp: new Date().toISOString(),
805
- };
806
- const server = createServerWithUnread({
807
- directMessages: [delegateMessage, normalMessage],
808
- groupMessages: [],
809
- }, {
810
- getStatusReporter: () => ({
811
- reportToolUse: () => statusEvents.push("tool_using"),
812
- reportToolResult: (r) => {
813
- statusEvents.push("tool_result");
814
- reportedResults.push(r);
815
- },
816
- reportThinking: () => statusEvents.push("thinking"),
817
- reportWaitingInput: () => statusEvents.push("waiting_input"),
818
- reportError: () => statusEvents.push("error"),
819
- reportToolError: () => statusEvents.push("tool_error"),
820
- }),
821
- });
822
- const handleToolCall = getHandleToolCall(server);
823
- const result = (await handleToolCall.call(server, "poll_message", {}));
824
- assert.equal(result.directMessages.length, 2);
825
- assert.deepEqual(statusEvents, ["tool_using", "tool_result", "thinking"]);
826
- assert.equal(reportedResults.length, 1);
827
- const reported = reportedResults[0];
828
- assert.equal(reported.directMessages.length, 1);
829
- assert.equal(reported.directMessages[0].msgId, "dm-normal-work");
830
- });
831
- });
832
- describe("McpServer memory store paths", () => {
833
- it("uses project permanent memory directory by default", async () => {
834
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-paths-"));
835
- try {
836
- const paths = withTemporaryEnv({
837
- AWS_WORKSPACE_PATH: dir,
838
- AWS_ROLE_NAME: "Backend/Engineer:Lead",
839
- AWS_MEMORY_STORE_PATH: undefined,
840
- AWS_ROLE_MEMORY_STORE_PATH: undefined,
841
- AWS_INSTANCE_MEMORY_STORE_PATH: undefined,
842
- AWS_AGENT_ID: undefined,
843
- }, () => Reflect.get(McpServer, "resolveMemoryStorePaths").call(McpServer));
844
- assert.equal(paths.rolePath, path.join(dir, ".agentswork", "memory", "memory", "Backend_Engineer_Lead"));
845
- assert.equal(paths.instancePath, path.join(dir, ".agentswork", "memory", "server-managed"));
846
- }
847
- finally {
848
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
849
- }
850
- });
851
- it("falls back to a default role memory directory when role name is blank", async () => {
852
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-memory-paths-"));
853
- try {
854
- const paths = withTemporaryEnv({
855
- AWS_WORKSPACE_PATH: dir,
856
- AWS_ROLE_NAME: " ",
857
- AWS_MEMORY_STORE_PATH: undefined,
858
- AWS_ROLE_MEMORY_STORE_PATH: undefined,
859
- AWS_INSTANCE_MEMORY_STORE_PATH: undefined,
860
- AWS_AGENT_ID: undefined,
861
- }, () => Reflect.get(McpServer, "resolveMemoryStorePaths").call(McpServer));
862
- assert.equal(paths.rolePath, path.join(dir, ".agentswork", "memory", "memory", "default-role"));
863
- }
864
- finally {
865
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
866
- }
867
- });
868
- });
869
- describe("McpServer memory tools", () => {
870
- it("exposes memory tools in MCP tool catalog", () => {
871
- const toolNames = MCP_TOOLS.map((tool) => tool.name);
872
- assert.ok(toolNames.includes("create_memory"));
873
- assert.ok(toolNames.includes("read_memory"));
874
- assert.ok(toolNames.includes("update_memory"));
875
- assert.ok(toolNames.includes("delete_memory"));
876
- assert.ok(toolNames.includes("search_memory"));
877
- assert.ok(toolNames.includes("list_memories"));
878
- assert.ok(toolNames.includes("get_memory_stats"));
879
- // Memory 近期记忆过期与回忆工具 (v1.11.0)
880
- assert.ok(toolNames.includes("memory_recurrence"));
881
- assert.ok(toolNames.includes("memory_recurrence_complete"));
882
- });
883
- it("creates, reads, updates, searches, and deletes memory through MCP dispatch", async () => {
884
- await withMemoryServer(async (server) => {
885
- const handleToolCall = getHandleToolCall(server);
886
- const created = await handleToolCall.call(server, "create_memory", {
887
- memory_type: "project_memory",
888
- uri: "Core://Agent/Profile/",
889
- content: "prefers concise backend summaries",
890
- });
891
- assert.equal(created.uri, "core://Agent/Profile");
892
- assert.equal(created.content, undefined);
893
- const read = await handleToolCall.call(server, "read_memory", {
894
- uri: "core://Agent/Profile",
895
- memory_type: "project_memory",
896
- });
897
- assert.equal(read.content, "prefers concise backend summaries");
898
- const updated = await handleToolCall.call(server, "update_memory", {
899
- uri: "core://Agent/Profile",
900
- memory_type: "project_memory",
901
- old_string: "concise",
902
- new_string: "structured",
903
- append: "\nuses MCP memory tools",
904
- });
905
- assert.equal(updated.content, undefined);
906
- assert.equal(updated.version, 2);
907
- const readUpdated = await handleToolCall.call(server, "read_memory", {
908
- uri: "core://Agent/Profile",
909
- memory_type: "project_memory",
910
- });
911
- assert.match(readUpdated.content, /structured backend/);
912
- assert.match(readUpdated.content, /MCP memory tools/);
913
- const searchResult = await handleToolCall.call(server, "search_memory", {
914
- query: "backend",
915
- limit: 5,
916
- });
917
- assert.equal(searchResult.results.length, 1);
918
- const deleted = await handleToolCall.call(server, "delete_memory", {
919
- uri: "core://Agent/Profile",
920
- memory_type: "project_memory",
921
- });
922
- assert.deepEqual(deleted, { deleted: true });
923
- });
924
- });
925
- it("delete_memory supports batch deletion via memories array", async () => {
926
- await withMemoryServer(async (server) => {
927
- const handleToolCall = getHandleToolCall(server);
928
- // 准备 4 条不同类型的记忆(模拟过期回忆流程中跨类型 recalling 场景)
929
- await handleToolCall.call(server, "create_memory", {
930
- memory_type: "recently_memory",
931
- uri: "task://batch/a",
932
- content: "a",
933
- });
934
- await handleToolCall.call(server, "create_memory", {
935
- memory_type: "agent_memory",
936
- uri: "agent://batch/b",
937
- content: "b",
938
- });
939
- await handleToolCall.call(server, "create_memory", {
940
- memory_type: "project_memory",
941
- uri: "core://batch/c",
942
- content: "c",
943
- });
944
- await handleToolCall.call(server, "create_memory", {
945
- memory_type: "agent_memory",
946
- uri: "agent://batch/d",
947
- content: "d",
948
- });
949
- // 批量删除:跨 3 种类型 + 1 条不存在 + 1 条类型不匹配(用独立 uri 避免与已删项冲突)
950
- const result = await handleToolCall.call(server, "delete_memory", {
951
- memories: [
952
- { uri: "task://batch/a", memory_type: "recently_memory" },
953
- { uri: "agent://batch/b", memory_type: "agent_memory" },
954
- { uri: "core://batch/c", memory_type: "project_memory" },
955
- { uri: "task://batch/missing" }, // 不存在
956
- { uri: "agent://batch/d", memory_type: "project_memory" }, // 类型不匹配(d 实为 agent_memory)
957
- ],
958
- });
959
- const batch = result;
960
- assert.equal(batch.deleted, 3);
961
- assert.deepEqual(batch.not_found, ["task://batch/missing"]);
962
- assert.equal(batch.failed.length, 1);
963
- assert.equal(batch.failed[0].uri, "agent://batch/d");
964
- // 类型不匹配的那条仍然存在
965
- const stillThere = await handleToolCall.call(server, "read_memory", {
966
- uri: "agent://batch/d",
967
- });
968
- assert.ok(stillThere.uri);
969
- });
970
- });
971
- it("delete_memory batch supports omitted memory_type (auto-detect)", async () => {
972
- await withMemoryServer(async (server) => {
973
- const handleToolCall = getHandleToolCall(server);
974
- await handleToolCall.call(server, "create_memory", {
975
- memory_type: "recently_memory",
976
- uri: "task://auto/a",
977
- content: "a",
978
- });
979
- await handleToolCall.call(server, "create_memory", {
980
- memory_type: "agent_memory",
981
- uri: "agent://auto/b",
982
- content: "b",
983
- });
984
- // 全部省略 memory_type,由系统自动判定
985
- const result = await handleToolCall.call(server, "delete_memory", {
986
- memories: [{ uri: "task://auto/a" }, { uri: "agent://auto/b" }],
987
- });
988
- const batch = result;
989
- assert.equal(batch.deleted, 2);
990
- assert.deepEqual(batch.not_found, []);
991
- assert.equal(batch.failed.length, 0);
992
- });
993
- });
994
- it("delete_memory rejects when both uri and memories are provided", async () => {
995
- await withMemoryServer(async (server) => {
996
- const handleToolCall = getHandleToolCall(server);
997
- await assert.rejects(() => handleToolCall.call(server, "delete_memory", {
998
- uri: "task://x",
999
- memories: [{ uri: "task://y" }],
1000
- }), /uri 和 memories/);
1001
- });
1002
- });
1003
- it("delete_memory rejects when neither uri nor memories is provided", async () => {
1004
- await withMemoryServer(async (server) => {
1005
- const handleToolCall = getHandleToolCall(server);
1006
- await assert.rejects(() => handleToolCall.call(server, "delete_memory", {}), /uri 或 memories/);
1007
- });
1008
- });
1009
- it("lists task and recent memories while protecting long-term memory from list_memories", async () => {
1010
- await withMemoryServer(async (server) => {
1011
- const handleToolCall = getHandleToolCall(server);
1012
- await handleToolCall.call(server, "create_memory", {
1013
- memory_type: "recently_memory",
1014
- uri: "task://todo/current",
1015
- content: "finish memory endpoint tests",
1016
- });
1017
- await handleToolCall.call(server, "create_memory", {
1018
- memory_type: "agent_memory",
1019
- uri: "recent://discussion/memory-tools",
1020
- content: "memory tools should stay local for now",
1021
- });
1022
- const taskList = await handleToolCall.call(server, "list_memories", {
1023
- memory_type: "recently_memory",
1024
- });
1025
- assert.equal(taskList.length, 1);
1026
- const recentList = await handleToolCall.call(server, "list_memories", {
1027
- memory_type: "agent_memory",
1028
- });
1029
- assert.equal(recentList.length, 1);
1030
- await assert.rejects(() => handleToolCall.call(server, "list_memories", {
1031
- memory_type: "project_memory",
1032
- }), /Invalid memory_type/);
1033
- });
1034
- });
1035
- it("stores memories in the instance store and isolates them between agent instances", async () => {
1036
- await withScopedMemoryServers(async (firstServer, secondServer) => {
1037
- const firstHandleToolCall = getHandleToolCall(firstServer);
1038
- const secondHandleToolCall = getHandleToolCall(secondServer);
1039
- await firstHandleToolCall.call(firstServer, "create_memory", {
1040
- memory_type: "project_memory",
1041
- uri: "core://role/shared-profile",
1042
- content: "long-term memory of instance one",
1043
- });
1044
- await firstHandleToolCall.call(firstServer, "create_memory", {
1045
- memory_type: "agent_memory",
1046
- uri: "recent://instance/private-context",
1047
- content: "agent one private context",
1048
- });
1049
- // 当前架构:记忆统一落在实例 store(跨实例共享由服务端 ServerMemoryStore 承担),
1050
- // 另一个实例本地读不到第一条实例的任何记忆
1051
- const roleMemoryFromSecond = await secondHandleToolCall.call(secondServer, "read_memory", {
1052
- uri: "core://role/shared-profile",
1053
- memory_type: "project_memory",
1054
- });
1055
- const recentMemoryFromSecond = await secondHandleToolCall.call(secondServer, "read_memory", {
1056
- uri: "recent://instance/private-context",
1057
- memory_type: "agent_memory",
1058
- });
1059
- assert.equal(roleMemoryFromSecond, null);
1060
- assert.equal(recentMemoryFromSecond, null);
1061
- // 写入实例自己可以正常读回
1062
- const ownRoleMemory = await firstHandleToolCall.call(firstServer, "read_memory", {
1063
- uri: "core://role/shared-profile",
1064
- memory_type: "project_memory",
1065
- });
1066
- assert.equal(ownRoleMemory.content, "long-term memory of instance one");
1067
- });
1068
- });
1069
- it("returns memory stats through MCP dispatch", async () => {
1070
- await withMemoryServer(async (server) => {
1071
- const handleToolCall = getHandleToolCall(server);
1072
- await handleToolCall.call(server, "create_memory", {
1073
- memory_type: "recently_memory",
1074
- uri: "task://todo/stats",
1075
- content: "temporary task",
1076
- });
1077
- await handleToolCall.call(server, "create_memory", {
1078
- memory_type: "agent_memory",
1079
- uri: "recent://note/stats",
1080
- content: "hot context",
1081
- });
1082
- await handleToolCall.call(server, "create_memory", {
1083
- memory_type: "project_memory",
1084
- uri: "core://agent/profile",
1085
- content: "long-term profile",
1086
- });
1087
- const stats = await handleToolCall.call(server, "get_memory_stats", {});
1088
- const typedStats = stats;
1089
- assert.equal(typedStats.total, 3);
1090
- assert.deepEqual(typedStats.listable, {
1091
- count: 2,
1092
- limit: 64,
1093
- hardLimit: 128,
1094
- exceeded: false,
1095
- hardExceeded: false,
1096
- });
1097
- assert.equal(typedStats.by_type.recently_memory.count, 1);
1098
- assert.equal(typedStats.by_type.recently_memory.domainCount, 1);
1099
- assert.equal(typedStats.by_type.recently_memory.by_domain.task?.count, 1);
1100
- assert.equal(typedStats.by_type.agent_memory.count, 1);
1101
- assert.equal(typedStats.by_type.agent_memory.domainCount, 1);
1102
- assert.equal(typedStats.by_type.agent_memory.by_domain.recent?.count, 1);
1103
- assert.equal(typedStats.by_type.project_memory.count, 1);
1104
- assert.equal(typedStats.by_type.project_memory.domainCount, 1);
1105
- assert.equal(typedStats.by_type.project_memory.by_domain.core?.count, 1);
1106
- });
1107
- });
1108
- it("reports combined memory stats after long-term memory changes", async () => {
1109
- await withMemoryServer(async (server) => {
1110
- const handleToolCall = getHandleToolCall(server);
1111
- const reportedStats = [];
1112
- attachMemoryStatsReporter(server, reportedStats);
1113
- await handleToolCall.call(server, "create_memory", {
1114
- memory_type: "project_memory",
1115
- uri: "project://test-aws/permanent-memory-check",
1116
- content: "dashboard should see this permanent memory count",
1117
- });
1118
- assert.equal(reportedStats.length, 1);
1119
- assert.equal(reportedStats[0]?.by_type.project_memory.count, 1);
1120
- assert.equal(reportedStats[0]?.by_type.project_memory.by_domain.project?.count, 1);
1121
- });
1122
- });
1123
- it("reports existing combined memory stats after auto registration", async () => {
1124
- await withMemoryServer(async (server) => {
1125
- const reportedStats = [];
1126
- // 当前架构:统计只来自实例 store(roleMemoryStore 合并统计已随双库架构移除)
1127
- const instanceStore = Reflect.get(server, "memoryStore");
1128
- await instanceStore.createMemory({
1129
- memory_type: "project_memory",
1130
- uri: "project://test-aws/existing-permanent-memory",
1131
- content: "existing role memory should be visible on dashboard startup",
1132
- });
1133
- // profileSynced=true 阻止 activelySyncProfile 异步调用 getProfile(mock 缺失该方法);
1134
- // maybeSyncPermanentMemoryToServer 覆盖为空函数避免异步操作干扰 tmpdir cleanup。
1135
- Reflect.set(server, "hasRoleName", true);
1136
- Reflect.set(server, "profileSynced", true);
1137
- Reflect.set(server, "maybeSyncPermanentMemoryToServer", () => { });
1138
- Reflect.set(server, "agentClient", {
1139
- getAgentConfig: () => ({ roleName: "backend", agentId: "agent-1" }),
1140
- register: async () => ({
1141
- agentId: "agent-1",
1142
- displayName: "Backend Agent",
1143
- status: "online",
1144
- }),
1145
- start: () => undefined,
1146
- isInDegradedMode: () => false,
1147
- getStatusReporter: () => null,
1148
- reportMemoryStats: async (memoryStats) => {
1149
- reportedStats.push(memoryStats);
1150
- },
1151
- });
1152
- const autoRegister = Reflect.get(server, "autoRegister");
1153
- await autoRegister.call(server);
1154
- assert.equal(reportedStats.length, 1);
1155
- assert.equal(reportedStats[0]?.by_type.project_memory.count, 1);
1156
- assert.equal(reportedStats[0]?.by_type.project_memory.by_domain.project?.count, 1);
1157
- });
1158
- });
1159
- it("returns memory stats for one requested memory type", async () => {
1160
- await withMemoryServer(async (server) => {
1161
- const handleToolCall = getHandleToolCall(server);
1162
- await handleToolCall.call(server, "create_memory", {
1163
- memory_type: "recently_memory",
1164
- uri: "task://todo/stats",
1165
- content: "temporary task",
1166
- });
1167
- await handleToolCall.call(server, "create_memory", {
1168
- memory_type: "agent_memory",
1169
- uri: "recent://note/stats",
1170
- content: "hot context",
1171
- });
1172
- await handleToolCall.call(server, "create_memory", {
1173
- memory_type: "project_memory",
1174
- uri: "core://agent/profile",
1175
- content: "long-term profile",
1176
- });
1177
- const stats = (await handleToolCall.call(server, "get_memory_stats", {
1178
- memory_type: "agent_memory",
1179
- }));
1180
- assert.equal(stats.memory_type, "agent_memory");
1181
- assert.equal(stats.total, 1);
1182
- assert.deepEqual(Object.keys(stats.by_type), ["agent_memory"]);
1183
- assert.equal(stats.by_type.agent_memory?.domainCount, 1);
1184
- assert.equal(stats.by_type.agent_memory?.by_domain.recent?.count, 1);
1185
- const memoryStats = (await handleToolCall.call(server, "get_memory_stats", { memory_type: "project_memory" }));
1186
- assert.equal(memoryStats.memory_type, "project_memory");
1187
- assert.equal(memoryStats.total, 1);
1188
- assert.deepEqual(Object.keys(memoryStats.by_type), ["project_memory"]);
1189
- assert.equal(memoryStats.by_type.project_memory?.domainCount, 1);
1190
- assert.equal(memoryStats.by_type.project_memory?.by_domain.core?.count, 1);
1191
- });
1192
- });
1193
- it("poll_message returns cleanup notice as a while-type when no unread messages", async () => {
1194
- await withMemoryServer(async (server) => {
1195
- const handleToolCall = getHandleToolCall(server);
1196
- Reflect.set(server, "agentClient", {
1197
- fetchUnreadMessages: async () => ({
1198
- directMessages: [],
1199
- groupMessages: [],
1200
- }),
1201
- getState: () => ({ agentId: "agent-self" }),
1202
- getStatusReporter: () => null,
1203
- });
1204
- for (let index = 0; index < 33; index += 1) {
1205
- await handleToolCall.call(server, "create_memory", {
1206
- memory_type: "agent_memory",
1207
- uri: `recent://cleanup/${index}`,
1208
- content: `recent ${index}`,
1209
- });
1210
- }
1211
- // Override poll to resolve immediately with empty (simulating timeout without waiting 30s)
1212
- const buffer = getMessageBuffer(server);
1213
- let pollCallCount = 0;
1214
- buffer.poll = ((_timeoutMs) => {
1215
- pollCallCount++;
1216
- return Promise.resolve({ directMessages: [], groupMessages: [] });
1217
- });
1218
- const result = (await handleToolCall.call(server, "poll_message", {}));
1219
- assert.equal(result.groupMessages.length, 0);
1220
- // cleanup notice returned as a while-type (no unread messages)
1221
- assert.equal(result.directMessages.length, 1);
1222
- assert.match(result.directMessages[0]?.msgId, /^memory-cleanup-/);
1223
- assert.equal(result.directMessages[0]?.senderId, "MCP-System");
1224
- assert.equal(result.directMessages[0]?.receiverId, "agent-self");
1225
- assert.match(result.directMessages[0]?.content ?? "", /本地 memory 已超过建议容量/);
1226
- assert.match(result.directMessages[0]?.content ?? "", /get_memory_stats/);
1227
- assert.equal(result.directMessages[0]?.prompt, undefined);
1228
- // 非紧急类在初始路径直接返回,不进入 while 循环
1229
- assert.equal(pollCallCount, 0);
1230
- });
1231
- });
1232
- it("poll_message returns task notification without cleanup notice (while-type not stacked)", async () => {
1233
- await withMemoryServer(async (server) => {
1234
- const handleToolCall = getHandleToolCall(server);
1235
- const taskNotification = {
1236
- msgId: "task-notify-before-cleanup",
1237
- senderId: "MCP-System",
1238
- senderName: "系统",
1239
- receiverId: "agent-self",
1240
- content: "你有任务需要关注!cleanup 前已完成静默抢占。",
1241
- requireReply: false,
1242
- timestamp: new Date().toISOString(),
1243
- };
1244
- const fetchOptions = [];
1245
- Reflect.set(server, "agentClient", {
1246
- fetchUnreadMessages: async (options) => {
1247
- fetchOptions.push(options ?? {});
1248
- return {
1249
- directMessages: [taskNotification],
1250
- groupMessages: [],
1251
- };
1252
- },
1253
- getState: () => ({ agentId: "agent-self" }),
1254
- getStatusReporter: () => null,
1255
- });
1256
- for (let index = 0; index < 33; index += 1) {
1257
- await handleToolCall.call(server, "create_memory", {
1258
- memory_type: "agent_memory",
1259
- uri: `recent://cleanup-claim/${index}`,
1260
- content: `recent ${index}`,
1261
- });
1262
- }
1263
- const result = (await handleToolCall.call(server, "poll_message", {}));
1264
- // Task notification is returned as an unread message; cleanup is a while-type
1265
- // that is only checked when there are no unread messages - not stacked with messages.
1266
- assert.equal(result.groupMessages.length, 0);
1267
- assert.equal(result.directMessages.length, 1);
1268
- assert.equal(result.directMessages[0]?.msgId, taskNotification.msgId);
1269
- });
1270
- });
1271
- it("get_message injects a cleanup notice when listable memory exceeds soft limits", async () => {
1272
- await withMemoryServer(async (server) => {
1273
- const handleToolCall = getHandleToolCall(server);
1274
- Reflect.set(server, "agentClient", {
1275
- fetchUnreadMessages: async () => ({
1276
- directMessages: [],
1277
- groupMessages: [],
1278
- }),
1279
- getState: () => ({ agentId: "agent-self" }),
1280
- getStatusReporter: () => null,
1281
- });
1282
- for (let index = 0; index < 33; index += 1) {
1283
- await handleToolCall.call(server, "create_memory", {
1284
- memory_type: "agent_memory",
1285
- uri: `recent://cleanup-get/${index}`,
1286
- content: `recent ${index}`,
1287
- });
1288
- }
1289
- const result = (await handleToolCall.call(server, "get_message", {}));
1290
- assert.equal(result.fromBuffer, true);
1291
- assert.equal(result.groupMessages.length, 0);
1292
- assert.equal(result.directMessages.length, 1);
1293
- assert.match(result.directMessages[0]?.msgId, /^memory-cleanup-/);
1294
- assert.equal(result.directMessages[0]?.senderId, "MCP-System");
1295
- assert.equal(result.directMessages[0]?.receiverId, "agent-self");
1296
- assert.match(result.directMessages[0]?.content ?? "", /本地 memory 已超过建议容量/);
1297
- assert.match(result.directMessages[0]?.content ?? "", /agent_memory\/recent 类型下当前 33 条 memory/);
1298
- assert.equal(result.directMessages[0]?.prompt, undefined);
1299
- });
1300
- });
1301
- it("rejects incomplete memory update arguments", async () => {
1302
- await withMemoryServer(async (server) => {
1303
- const handleToolCall = getHandleToolCall(server);
1304
- await handleToolCall.call(server, "create_memory", {
1305
- memory_type: "project_memory",
1306
- uri: "core://agent/incomplete-update",
1307
- content: "original",
1308
- });
1309
- await assert.rejects(() => handleToolCall.call(server, "update_memory", {
1310
- uri: "core://agent/incomplete-update",
1311
- old_string: "original",
1312
- }), /old_string and new_string must be provided together/);
1313
- });
1314
- });
1315
- it("get_message with _aws_force_inject returns only force messages and keeps normal ones", async () => {
1316
- await withMemoryServer(async (server) => {
1317
- const handleToolCall = getHandleToolCall(server);
1318
- Reflect.set(server, "agentClient", {
1319
- fetchUnreadMessages: async () => ({
1320
- directMessages: [],
1321
- groupMessages: [],
1322
- }),
1323
- getState: () => ({ agentId: "agent-self" }),
1324
- getStatusReporter: () => null,
1325
- });
1326
- const messageBuffer = getMessageBuffer(server);
1327
- messageBuffer.push({
1328
- msgId: "dm-force-1",
1329
- senderId: "human-1",
1330
- senderName: "Human One",
1331
- content: "紧急消息",
1332
- requireReply: false,
1333
- force: true,
1334
- timestamp: new Date().toISOString(),
1335
- });
1336
- messageBuffer.push({
1337
- msgId: "dm-normal-1",
1338
- senderId: "agent-2",
1339
- senderName: "Agent Two",
1340
- content: "普通消息",
1341
- requireReply: false,
1342
- timestamp: new Date().toISOString(),
1343
- });
1344
- // runtime 迭代边界强制注入调用:只取 force=true 的私信
1345
- const result = (await handleToolCall.call(server, "get_message", {
1346
- _aws_force_inject: true,
1347
- }));
1348
- assert.equal(result.directMessages.length, 1);
1349
- assert.equal(result.directMessages[0]?.msgId, "dm-force-1");
1350
- assert.equal(result.groupMessages.length, 0);
1351
- // 普通消息留在缓冲区,下一次 get_message 正常返回
1352
- const rest = (await handleToolCall.call(server, "get_message", {}));
1353
- assert.equal(rest.directMessages.length, 1);
1354
- assert.equal(rest.directMessages[0]?.msgId, "dm-normal-1");
1355
- });
1356
- });
1357
- it("redacts memory content from status reporting", async () => {
1358
- await withMemoryServer(async (server) => {
1359
- const reported = [];
1360
- const statusReporter = {
1361
- reportToolUse: (name, args) => reported.push({ name, args }),
1362
- reportToolResult: () => undefined,
1363
- reportToolError: () => undefined,
1364
- reportThinking: () => undefined,
1365
- reportWaitingInput: () => undefined,
1366
- reportError: () => undefined,
1367
- };
1368
- Reflect.set(server, "agentClient", {
1369
- getStatusReporter: () => statusReporter,
1370
- });
1371
- const handleToolCall = getHandleToolCall(server);
1372
- await handleToolCall.call(server, "create_memory", {
1373
- memory_type: "project_memory",
1374
- uri: "core://agent/private",
1375
- content: "secret local memory content",
1376
- });
1377
- await handleToolCall.call(server, "update_memory", {
1378
- uri: "core://agent/private",
1379
- old_string: "secret",
1380
- new_string: "private",
1381
- append: "\nnever report this append",
1382
- });
1383
- await handleToolCall.call(server, "search_memory", {
1384
- query: "private search phrase",
1385
- limit: 3,
1386
- });
1387
- assert.deepEqual(reported[0], {
1388
- name: "create_memory",
1389
- args: { uri: "core://agent/private", memory_type: "project_memory" },
1390
- });
1391
- assert.deepEqual(reported[1], {
1392
- name: "update_memory",
1393
- args: { uri: "core://agent/private" },
1394
- });
1395
- assert.deepEqual(reported[2], {
1396
- name: "search_memory",
1397
- args: { limit: 3, queryProvided: true },
1398
- });
1399
- });
1400
- });
1401
- it("keeps normal non-memory status reporting unchanged", async () => {
1402
- const server = new McpServer();
1403
- const reported = [];
1404
- const statusReporter = {
1405
- reportToolUse: (name, args) => reported.push({ name, args }),
1406
- reportToolResult: () => undefined,
1407
- reportToolError: () => undefined,
1408
- reportThinking: () => undefined,
1409
- reportWaitingInput: () => undefined,
1410
- reportError: () => undefined,
1411
- };
1412
- Reflect.set(server, "agentClient", {
1413
- sendGroupMessage: async () => ({
1414
- msgId: 1,
1415
- timestamp: new Date().toISOString(),
1416
- }),
1417
- getStatusReporter: () => statusReporter,
1418
- });
1419
- const handleToolCall = getHandleToolCall(server);
1420
- await handleToolCall.call(server, "send_group", {
1421
- content: "normal message",
1422
- roomId: "project:demo",
1423
- });
1424
- assert.deepEqual(reported, [
1425
- {
1426
- name: "send_group",
1427
- args: { content: "normal message", roomId: "project:demo" },
1428
- },
1429
- ]);
1430
- });
1431
- it("rejects invalid memory type before writing", async () => {
1432
- await withMemoryServer(async (server) => {
1433
- const handleToolCall = getHandleToolCall(server);
1434
- await assert.rejects(() => handleToolCall.call(server, "create_memory", {
1435
- memory_type: "unknown",
1436
- uri: "core://agent/invalid-type",
1437
- content: "should not write",
1438
- }), /Invalid memory_type/);
1439
- });
1440
- });
1441
- it("rejects duplicate create and returns deleted false for missing memory", async () => {
1442
- await withMemoryServer(async (server) => {
1443
- const handleToolCall = getHandleToolCall(server);
1444
- await handleToolCall.call(server, "create_memory", {
1445
- memory_type: "project_memory",
1446
- uri: "core://agent/duplicate",
1447
- content: "first",
1448
- });
1449
- await assert.rejects(() => handleToolCall.call(server, "create_memory", {
1450
- memory_type: "project_memory",
1451
- uri: "core://agent/duplicate",
1452
- content: "second",
1453
- }), /Memory already exists/);
1454
- const missingDelete = await handleToolCall.call(server, "delete_memory", {
1455
- uri: "core://agent/not-found",
1456
- });
1457
- assert.deepEqual(missingDelete, { deleted: false });
1458
- });
1459
- });
1460
- it("allows no-content update for status-only refresh (empty append)", async () => {
1461
- await withMemoryServer(async (server) => {
1462
- const handleToolCall = getHandleToolCall(server);
1463
- await handleToolCall.call(server, "create_memory", {
1464
- memory_type: "project_memory",
1465
- uri: "core://agent/noop",
1466
- content: "original",
1467
- });
1468
- // 空 append 不再报错,而是执行无内容更新(仅刷新状态/MD5)
1469
- const result = await handleToolCall.call(server, "update_memory", {
1470
- uri: "core://agent/noop",
1471
- append: "",
1472
- });
1473
- assert.ok(result, "no-content update should succeed");
1474
- });
1475
- });
1476
- it("rejects create_memory when bound_files contain non-existent paths", async () => {
1477
- await withMemoryServer(async (server) => {
1478
- const handleToolCall = getHandleToolCall(server);
1479
- await assert.rejects(() => handleToolCall.call(server, "create_memory", {
1480
- memory_type: "project_memory",
1481
- uri: "core://agent/missing-bound-file",
1482
- content: "memory with non-existent bound file",
1483
- bound_files: ["./non-existent-file.ts"],
1484
- }), /绑定的文件不存在/);
1485
- });
1486
- });
1487
- it("rejects update_memory when new bound_files contain non-existent paths", async () => {
1488
- await withMemoryServer(async (server) => {
1489
- const handleToolCall = getHandleToolCall(server);
1490
- await handleToolCall.call(server, "create_memory", {
1491
- memory_type: "project_memory",
1492
- uri: "core://agent/update-with-missing-bound-file",
1493
- content: "original content",
1494
- });
1495
- await assert.rejects(() => handleToolCall.call(server, "update_memory", {
1496
- uri: "core://agent/update-with-missing-bound-file",
1497
- memory_type: "project_memory",
1498
- old_string: "original",
1499
- new_string: "updated",
1500
- bound_files: ["./non-existent-file.ts"],
1501
- }), /绑定的文件不存在/);
1502
- });
1503
- });
1504
- it("allows create_memory with valid existing bound files", async () => {
1505
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-bound-file-test-"));
1506
- const testFilePath = path.join(dir, "existing-file.ts");
1507
- await writeFile(testFilePath, "test content", "utf-8");
1508
- const previousWorkspacePath = process.env.AWS_WORKSPACE_PATH;
1509
- process.env.AWS_WORKSPACE_PATH = dir;
1510
- try {
1511
- await withMemoryServer(async (server) => {
1512
- const handleToolCall = getHandleToolCall(server);
1513
- const created = await handleToolCall.call(server, "create_memory", {
1514
- memory_type: "project_memory",
1515
- uri: "core://agent/valid-bound-file",
1516
- content: "memory with valid bound file",
1517
- bound_files: ["./existing-file.ts"],
1518
- });
1519
- assert.equal(created.uri, "core://agent/valid-bound-file");
1520
- assert.deepEqual(created.bound_files, ["./existing-file.ts"]);
1521
- });
1522
- }
1523
- finally {
1524
- if (previousWorkspacePath === undefined) {
1525
- delete process.env.AWS_WORKSPACE_PATH;
1526
- }
1527
- else {
1528
- process.env.AWS_WORKSPACE_PATH = previousWorkspacePath;
1529
- }
1530
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
1531
- }
1532
- });
1533
- it("rejects create_memory with mixed existing and non-existent bound files", async () => {
1534
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-bound-file-mixed-"));
1535
- const testFilePath = path.join(dir, "existing-file.ts");
1536
- await writeFile(testFilePath, "test content", "utf-8");
1537
- const previousWorkspacePath = process.env.AWS_WORKSPACE_PATH;
1538
- process.env.AWS_WORKSPACE_PATH = dir;
1539
- try {
1540
- await withMemoryServer(async (server) => {
1541
- const handleToolCall = getHandleToolCall(server);
1542
- await assert.rejects(() => handleToolCall.call(server, "create_memory", {
1543
- memory_type: "project_memory",
1544
- uri: "core://agent/mixed-bound-file",
1545
- content: "memory with mixed bound files",
1546
- bound_files: ["./existing-file.ts", "./non-existent-file.ts"],
1547
- }), /non-existent-file/);
1548
- });
1549
- }
1550
- finally {
1551
- if (previousWorkspacePath === undefined) {
1552
- delete process.env.AWS_WORKSPACE_PATH;
1553
- }
1554
- else {
1555
- process.env.AWS_WORKSPACE_PATH = previousWorkspacePath;
1556
- }
1557
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
1558
- }
1559
- });
1560
- it("allows update_memory without bound_files when existing bound files are absent", async () => {
1561
- await withMemoryServer(async (server) => {
1562
- const handleToolCall = getHandleToolCall(server);
1563
- await handleToolCall.call(server, "create_memory", {
1564
- memory_type: "project_memory",
1565
- uri: "core://agent/update-without-bound-files",
1566
- content: "original content",
1567
- });
1568
- // Update without bound_files should succeed even if existing bound files are invalid
1569
- const updated = await handleToolCall.call(server, "update_memory", {
1570
- uri: "core://agent/update-without-bound-files",
1571
- memory_type: "project_memory",
1572
- old_string: "original",
1573
- new_string: "updated",
1574
- });
1575
- assert.equal(updated.version, 2);
1576
- });
1577
- });
1578
- it("normalizes Windows backslash bound_files to forward slashes on create", async () => {
1579
- const dir = await mkdtemp(path.join(os.tmpdir(), "aws-mcp-bound-file-backslash-"));
1580
- const subDir = path.join(dir, "src", "utils");
1581
- await (await import("node:fs/promises")).mkdir(subDir, { recursive: true });
1582
- const testFilePath = path.join(subDir, "file.ts");
1583
- await writeFile(testFilePath, "test content", "utf-8");
1584
- const previousWorkspacePath = process.env.AWS_WORKSPACE_PATH;
1585
- process.env.AWS_WORKSPACE_PATH = dir;
1586
- try {
1587
- await withMemoryServer(async (server) => {
1588
- const handleToolCall = getHandleToolCall(server);
1589
- const created = await handleToolCall.call(server, "create_memory", {
1590
- memory_type: "project_memory",
1591
- uri: "core://agent/backslash-bound-file",
1592
- content: "memory with Windows backslash bound file",
1593
- bound_files: ["src\\utils\\file.ts"],
1594
- });
1595
- const boundFiles = created.bound_files;
1596
- assert.deepEqual(boundFiles, ["./src/utils/file.ts"]);
1597
- });
1598
- }
1599
- finally {
1600
- if (previousWorkspacePath === undefined) {
1601
- delete process.env.AWS_WORKSPACE_PATH;
1602
- }
1603
- else {
1604
- process.env.AWS_WORKSPACE_PATH = previousWorkspacePath;
1605
- }
1606
- await rm(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
1607
- }
1608
- });
1609
- });
1610
- describe("McpServer group discovery tool", () => {
1611
- it("get_group_rooms should proxy discoverable groups from agent client", async () => {
1612
- const server = new McpServer();
1613
- Reflect.set(server, "agentClient", {
1614
- discoverGroupRooms: async () => ({
1615
- groups: [
1616
- {
1617
- id: "project:demo",
1618
- projectId: "demo",
1619
- roomType: "project",
1620
- scopeRefId: "demo",
1621
- name: "demo 公共群聊",
1622
- memberUserIds: ["agent-1"],
1623
- },
1624
- {
1625
- id: "inner_group:g-1",
1626
- projectId: "demo",
1627
- roomType: "inner_group",
1628
- scopeRefId: "g-1",
1629
- name: "后端小组",
1630
- memberUserIds: ["agent-1", "agent-2"],
1631
- },
1632
- ],
1633
- }),
1634
- getStatusReporter: () => null,
1635
- });
1636
- const handleToolCall = Reflect.get(server, "handleToolCall");
1637
- const result = await handleToolCall.call(server, "get_group_rooms", {});
1638
- assert.deepEqual(result, {
1639
- groups: [
1640
- {
1641
- id: "project:demo",
1642
- projectId: "demo",
1643
- roomType: "project",
1644
- scopeRefId: "demo",
1645
- name: "demo 公共群聊",
1646
- memberUserIds: ["agent-1"],
1647
- },
1648
- {
1649
- id: "inner_group:g-1",
1650
- projectId: "demo",
1651
- roomType: "inner_group",
1652
- scopeRefId: "g-1",
1653
- name: "后端小组",
1654
- memberUserIds: ["agent-1", "agent-2"],
1655
- },
1656
- ],
1657
- });
1658
- });
1659
- it("send_group should forward roomId to agent client", async () => {
1660
- const server = new McpServer();
1661
- let capturedArgs = null;
1662
- Reflect.set(server, "agentClient", {
1663
- sendGroupMessage: async (content, projectId, roomId) => {
1664
- capturedArgs = { content, projectId, roomId };
1665
- return {
1666
- msgId: 1,
1667
- timestamp: new Date().toISOString(),
1668
- };
1669
- },
1670
- getStatusReporter: () => null,
1671
- });
1672
- const handleToolCall = Reflect.get(server, "handleToolCall");
1673
- await handleToolCall.call(server, "send_group", {
1674
- content: "hello room",
1675
- projectId: "demo",
1676
- roomId: "inner_group:g-1",
1677
- });
1678
- assert.deepEqual(capturedArgs, {
1679
- content: "hello room",
1680
- projectId: "demo",
1681
- roomId: "inner_group:g-1",
1682
- });
1683
- });
1684
- it("hire_colleague should forward role creation payload to agent client", async () => {
1685
- const server = new McpServer();
1686
- let capturedArgs = null;
1687
- const hiredResult = {
1688
- id: "agent-new",
1689
- displayName: "后端开发同事-AB12",
1690
- roleName: "后端开发同事",
1691
- projectId: "demo",
1692
- workspacePath: "D:/workspace/demo",
1693
- runtimeBridgeBaseUrl: "http://127.0.0.1:18081",
1694
- runtimeStatus: "stopped",
1695
- };
1696
- Reflect.set(server, "agentClient", {
1697
- hireColleague: async (args) => {
1698
- capturedArgs = args;
1699
- return hiredResult;
1700
- },
1701
- // 「招聘工具」能力开启,通过执行期守卫(默认关闭)
1702
- getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
1703
- getStatusReporter: () => null,
1704
- });
1705
- const handleToolCall = Reflect.get(server, "handleToolCall");
1706
- const result = await handleToolCall.call(server, "hire_colleague", {
1707
- projectId: "demo",
1708
- roleName: "后端开发同事",
1709
- displayName: "后端开发同事-AB12",
1710
- prompt: "负责后端接口开发",
1711
- workspacePath: "D:/workspace/demo",
1712
- runtimeSkillEnabled: true,
1713
- runtimeMcpEnabled: true,
1714
- });
1715
- assert.deepEqual(capturedArgs, {
1716
- projectId: "demo",
1717
- roleName: "后端开发同事",
1718
- displayName: "后端开发同事-AB12",
1719
- prompt: "负责后端接口开发",
1720
- workspacePath: "D:/workspace/demo",
1721
- runtimeSkillEnabled: true,
1722
- runtimeMcpEnabled: true,
1723
- });
1724
- assert.deepEqual(result, hiredResult);
1725
- });
1726
- it("hire_colleague should validate required workspacePath", async () => {
1727
- const server = new McpServer();
1728
- // 「招聘工具」能力开启,通过执行期守卫(默认关闭)
1729
- Reflect.set(server, "agentClient", {
1730
- getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
1731
- getStatusReporter: () => null,
1732
- });
1733
- const handleToolCall = Reflect.get(server, "handleToolCall");
1734
- await assert.rejects(async () => {
1735
- await handleToolCall.call(server, "hire_colleague", {
1736
- roleName: "后端开发同事",
1737
- });
1738
- }, /workspacePath is required/);
1739
- });
1740
- it("hire_colleagues_batch should forward batch payload to agent client", async () => {
1741
- const server = new McpServer();
1742
- let capturedArgs = null;
1743
- const batchResult = {
1744
- batchId: "hire-batch-001",
1745
- projectId: "demo",
1746
- summary: {
1747
- total: 2,
1748
- approved: 1,
1749
- rejected: 1,
1750
- created: 1,
1751
- failed: 0,
1752
- },
1753
- results: [
1754
- {
1755
- itemId: "item-1",
1756
- decision: "APPROVED",
1757
- status: "CREATED",
1758
- createdAgent: {
1759
- id: "agent-new",
1760
- displayName: "后端开发同事-AB12",
1761
- roleName: "后端开发同事",
1762
- },
1763
- },
1764
- {
1765
- itemId: "item-2",
1766
- decision: "REJECTED",
1767
- status: "REJECTED",
1768
- reason: "当前阶段暂不需要",
1769
- },
1770
- ],
1771
- };
1772
- Reflect.set(server, "agentClient", {
1773
- hireColleaguesBatch: async (args) => {
1774
- capturedArgs = args;
1775
- return batchResult;
1776
- },
1777
- // 「招聘工具」能力开启,通过执行期守卫(默认关闭)
1778
- getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
1779
- getStatusReporter: () => null,
1780
- });
1781
- const handleToolCall = Reflect.get(server, "handleToolCall");
1782
- const result = await handleToolCall.call(server, "hire_colleagues_batch", {
1783
- projectId: "demo",
1784
- items: [
1785
- {
1786
- roleName: "后端开发同事",
1787
- displayName: "后端开发同事-AB12",
1788
- prompt: "负责后端接口开发",
1789
- workspacePath: "D:/workspace/demo/backend",
1790
- },
1791
- {
1792
- newRoleName: "测试开发同事",
1793
- displayName: "测试开发同事-CD34",
1794
- prompt: "负责接口测试",
1795
- workspacePath: "D:/workspace/demo/test",
1796
- },
1797
- ],
1798
- });
1799
- assert.deepEqual(capturedArgs, {
1800
- projectId: "demo",
1801
- items: [
1802
- {
1803
- roleName: "后端开发同事",
1804
- displayName: "后端开发同事-AB12",
1805
- prompt: "负责后端接口开发",
1806
- workspacePath: "D:/workspace/demo/backend",
1807
- },
1808
- {
1809
- newRoleName: "测试开发同事",
1810
- displayName: "测试开发同事-CD34",
1811
- prompt: "负责接口测试",
1812
- workspacePath: "D:/workspace/demo/test",
1813
- },
1814
- ],
1815
- });
1816
- assert.deepEqual(result, batchResult);
1817
- });
1818
- it("hire_colleagues_batch should validate required items", async () => {
1819
- const server = new McpServer();
1820
- // 「招聘工具」能力开启,通过执行期守卫(默认关闭)
1821
- Reflect.set(server, "agentClient", {
1822
- getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
1823
- getStatusReporter: () => null,
1824
- });
1825
- const handleToolCall = Reflect.get(server, "handleToolCall");
1826
- await assert.rejects(async () => {
1827
- await handleToolCall.call(server, "hire_colleagues_batch", {
1828
- projectId: "demo",
1829
- items: [],
1830
- });
1831
- }, /items is required/);
1832
- });
1833
- it("hire_colleagues_batch should validate every item workspacePath", async () => {
1834
- const server = new McpServer();
1835
- // 「招聘工具」能力开启,通过执行期守卫(默认关闭)
1836
- Reflect.set(server, "agentClient", {
1837
- getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
1838
- getStatusReporter: () => null,
1839
- });
1840
- const handleToolCall = Reflect.get(server, "handleToolCall");
1841
- await assert.rejects(async () => {
1842
- await handleToolCall.call(server, "hire_colleagues_batch", {
1843
- projectId: "demo",
1844
- items: [
1845
- {
1846
- roleName: "后端开发同事",
1847
- workspacePath: "D:/workspace/demo/backend",
1848
- },
1849
- {
1850
- roleName: "测试开发同事",
1851
- },
1852
- ],
1853
- });
1854
- }, /items\[1\].workspacePath is required/);
1855
- });
1856
- });
1857
- describe("McpServer incoming group message filtering", () => {
1858
- it("should ignore self-sent group messages pushed from websocket events", async () => {
1859
- const server = new McpServer();
1860
- Reflect.set(server, "messageBuffer", new MessageBuffer());
1861
- Reflect.set(server, "agentClient", {
1862
- getState: () => ({
1863
- agentId: "agent-self",
1864
- displayName: "Self Agent",
1865
- isConnected: true,
1866
- isRegistered: true,
1867
- }),
1868
- getStatusReporter: () => null,
1869
- });
1870
- const selfGroupMessage = {
1871
- msgId: 501,
1872
- senderId: "agent-self",
1873
- senderName: "Self Agent",
1874
- content: "loopback group message",
1875
- timestamp: new Date().toISOString(),
1876
- projectId: "demo",
1877
- roomId: "project:demo",
1878
- };
1879
- const handleIncomingMessage = Reflect.get(server, "handleIncomingMessage");
1880
- await handleIncomingMessage.call(server, selfGroupMessage);
1881
- const messageBuffer = getMessageBuffer(server);
1882
- const drained = messageBuffer.get();
1883
- assert.deepEqual(drained, {
1884
- directMessages: [],
1885
- groupMessages: [],
1886
- });
1887
- });
1888
- it("should ignore persisted pipeline task assignment direct messages from websocket events", async () => {
1889
- const server = new McpServer();
1890
- Reflect.set(server, "messageBuffer", new MessageBuffer());
1891
- Reflect.set(server, "agentClient", {
1892
- getState: () => ({
1893
- agentId: "agent-loser",
1894
- displayName: "Loser Agent",
1895
- isConnected: true,
1896
- isRegistered: true,
1897
- }),
1898
- getStatusReporter: () => null,
1899
- });
1900
- const taskAssignedMessage = {
1901
- msgId: "task-assigned-1",
1902
- senderId: "MCP-System",
1903
- senderName: "MCP System",
1904
- receiverId: "agent-loser",
1905
- content: "🔔 [系统通知] 您有 1 个新的流程线任务正在等待处理。\n{\"type\":\"pipeline_task_assigned\",\"nodeId\":\"node-1\"}",
1906
- requireReply: false,
1907
- timestamp: new Date().toISOString(),
1908
- };
1909
- const handleIncomingMessage = Reflect.get(server, "handleIncomingMessage");
1910
- await handleIncomingMessage.call(server, taskAssignedMessage);
1911
- const messageBuffer = getMessageBuffer(server);
1912
- const drained = messageBuffer.get();
1913
- assert.deepEqual(drained, {
1914
- directMessages: [],
1915
- groupMessages: [],
1916
- });
1917
- });
1918
- });
1919
- describe("McpServer unread direct message filtering", () => {
1920
- it("should not return persisted pipeline task assignment messages from local buffer", async () => {
1921
- const taskAssignedMessage = {
1922
- msgId: "task-assigned-2",
1923
- senderId: "MCP-System",
1924
- senderName: "MCP System",
1925
- receiverId: "agent-loser",
1926
- content: "🔔 [系统通知] 您有 1 个新的流程线任务正在等待处理。\n{\"type\":\"pipeline_task_assigned\",\"nodeId\":\"node-1\"}",
1927
- requireReply: false,
1928
- prompt: "pipeline_task_assigned wake signal",
1929
- timestamp: new Date().toISOString(),
1930
- };
1931
- const normalMessage = {
1932
- msgId: "normal-1",
1933
- senderId: "human-1",
1934
- senderName: "Human",
1935
- receiverId: "agent-loser",
1936
- content: "normal direct message",
1937
- requireReply: false,
1938
- timestamp: new Date().toISOString(),
1939
- };
1940
- const server = createServerWithUnread({
1941
- directMessages: [taskAssignedMessage, normalMessage],
1942
- groupMessages: [],
1943
- });
1944
- const handleToolCall = Reflect.get(server, "handleToolCall");
1945
- const result = (await handleToolCall.call(server, "get_message", {}));
1946
- assert.equal(result.directMessages.length, 1);
1947
- assert.equal(result.directMessages[0]?.msgId, "normal-1");
1948
- assert.equal(result.groupMessages.length, 0);
1949
- });
1950
- });
1951
- describe("AgentClient group room aggregation", () => {
1952
- it("fetchUnreadMessages should aggregate unread messages from all discoverable rooms", async () => {
1953
- const client = new AgentClient();
1954
- Reflect.set(client, "agentId", "agent-1");
1955
- Reflect.set(client, "projectId", "demo");
1956
- const calls = [];
1957
- Reflect.set(client, "httpClient", {
1958
- callTool: async (toolName, args) => {
1959
- calls.push({ toolName, args });
1960
- if (toolName === "get_dm_messages") {
1961
- return { messages: [] };
1962
- }
1963
- if (toolName === "discover_group_rooms") {
1964
- return {
1965
- groups: [
1966
- {
1967
- id: "project:demo",
1968
- projectId: "demo",
1969
- roomType: "project",
1970
- scopeRefId: "demo",
1971
- name: "demo 公共群聊",
1972
- memberUserIds: ["agent-1"],
1973
- },
1974
- {
1975
- id: "inner_group:g-1",
1976
- projectId: "demo",
1977
- roomType: "inner_group",
1978
- scopeRefId: "g-1",
1979
- name: "后端小组",
1980
- memberUserIds: ["agent-1", "agent-2"],
1981
- },
1982
- ],
1983
- };
1984
- }
1985
- if (toolName === "get_group_messages") {
1986
- if (args.roomId === "project:demo") {
1987
- return {
1988
- messages: [
1989
- {
1990
- msgId: 1,
1991
- senderId: "agent-a",
1992
- senderName: "Agent A",
1993
- projectId: "demo",
1994
- roomId: "project:demo",
1995
- content: "project message",
1996
- timestamp: new Date().toISOString(),
1997
- },
1998
- ],
1999
- currentReadPos: 1,
2000
- hasMore: false,
2001
- };
2002
- }
2003
- if (args.roomId === "inner_group:g-1") {
2004
- return {
2005
- messages: [
2006
- {
2007
- msgId: 2,
2008
- senderId: "agent-b",
2009
- senderName: "Agent B",
2010
- projectId: "demo",
2011
- roomId: "inner_group:g-1",
2012
- content: "inner group message",
2013
- timestamp: new Date().toISOString(),
2014
- },
2015
- ],
2016
- currentReadPos: 2,
2017
- hasMore: false,
2018
- };
2019
- }
2020
- }
2021
- throw new Error(`unexpected tool call: ${toolName}`);
2022
- },
2023
- });
2024
- const result = await client.fetchUnreadMessages();
2025
- assert.equal(result.directMessages.length, 0);
2026
- assert.deepEqual(result.groupMessages.map((message) => ({
2027
- roomId: message.roomId,
2028
- content: message.content,
2029
- })), [
2030
- { roomId: "project:demo", content: "project message" },
2031
- { roomId: "inner_group:g-1", content: "inner group message" },
2032
- ]);
2033
- assert.equal(calls.filter((call) => call.toolName === "get_group_messages").length, 2);
2034
- });
2035
- it("getGroupHistory should aggregate history from all discoverable rooms when roomId is omitted", async () => {
2036
- const client = new AgentClient();
2037
- Reflect.set(client, "agentId", "agent-1");
2038
- Reflect.set(client, "projectId", "demo");
2039
- Reflect.set(client, "httpClient", {
2040
- callTool: async (toolName, args) => {
2041
- if (toolName === "discover_group_rooms") {
2042
- return {
2043
- groups: [
2044
- {
2045
- id: "project:demo",
2046
- projectId: "demo",
2047
- roomType: "project",
2048
- scopeRefId: "demo",
2049
- name: "demo 公共群聊",
2050
- memberUserIds: ["agent-1"],
2051
- },
2052
- {
2053
- id: "inner_group:g-1",
2054
- projectId: "demo",
2055
- roomType: "inner_group",
2056
- scopeRefId: "g-1",
2057
- name: "后端小组",
2058
- memberUserIds: ["agent-1", "agent-2"],
2059
- },
2060
- ],
2061
- };
2062
- }
2063
- if (toolName === "get_group_history") {
2064
- if (args.roomId === "project:demo") {
2065
- return {
2066
- messages: [
2067
- {
2068
- msgId: 1,
2069
- senderId: "agent-a",
2070
- senderName: "Agent A",
2071
- projectId: "demo",
2072
- roomId: "project:demo",
2073
- content: "project history",
2074
- timestamp: "2026-03-25T10:00:00.000Z",
2075
- },
2076
- ],
2077
- count: 1,
2078
- };
2079
- }
2080
- if (args.roomId === "inner_group:g-1") {
2081
- return {
2082
- messages: [
2083
- {
2084
- msgId: 2,
2085
- senderId: "agent-b",
2086
- senderName: "Agent B",
2087
- projectId: "demo",
2088
- roomId: "inner_group:g-1",
2089
- content: "group history",
2090
- timestamp: "2026-03-25T11:00:00.000Z",
2091
- },
2092
- ],
2093
- count: 1,
2094
- };
2095
- }
2096
- }
2097
- throw new Error(`unexpected tool call: ${toolName}`);
2098
- },
2099
- });
2100
- const result = await client.getGroupHistory(50);
2101
- assert.deepEqual(result.messages.map((message) => ({
2102
- roomId: message.roomId,
2103
- content: message.content,
2104
- })), [
2105
- { roomId: "project:demo", content: "project history" },
2106
- { roomId: "inner_group:g-1", content: "group history" },
2107
- ]);
2108
- assert.equal(result.count, 2);
2109
- });
2110
- });
2111
- describe("McpServer todo guard on poll_message", () => {
2112
- // 使用唯一 agentId 避免污染开发者真实 todo 文件;每次测试写入并清理。
2113
- // ACODE_HOME 指向临时目录:与被测的 resolveAcodeDataHome() 同源,不触碰真实 ~/.acode
2114
- const testAgentId = `test-todo-reminder-${process.pid}-${Date.now()}`;
2115
- let savedAcodeHome;
2116
- let acodeHome = "";
2117
- let todoDir = "";
2118
- before(async () => {
2119
- savedAcodeHome = process.env.ACODE_HOME;
2120
- acodeHome = await mkdtemp(path.join(os.tmpdir(), "acode-home-"));
2121
- process.env.ACODE_HOME = acodeHome;
2122
- todoDir = path.join(acodeHome, "todos");
2123
- });
2124
- after(async () => {
2125
- if (savedAcodeHome === undefined) {
2126
- delete process.env.ACODE_HOME;
2127
- }
2128
- else {
2129
- process.env.ACODE_HOME = savedAcodeHome;
2130
- }
2131
- await rm(acodeHome, { recursive: true, force: true });
2132
- });
2133
- const todoPath = () => path.join(todoDir, `${testAgentId}.json`);
2134
- async function writeTodoFile(todos) {
2135
- await mkdir(todoDir, { recursive: true });
2136
- await writeFile(todoPath(), JSON.stringify({
2137
- agentId: testAgentId,
2138
- updatedAt: new Date().toISOString(),
2139
- background: "test background",
2140
- todos,
2141
- }), "utf-8");
2142
- }
2143
- async function cleanupTodoFile() {
2144
- try {
2145
- await rm(todoPath(), { force: true });
2146
- }
2147
- catch {
2148
- // 忽略清理失败
2149
- }
2150
- }
2151
- function createServerForTodo() {
2152
- return createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2153
- getState: () => ({ agentId: testAgentId }),
2154
- });
2155
- }
2156
- function makeDirectMessage(msgId) {
2157
- return {
2158
- msgId,
2159
- senderId: "agent-wake",
2160
- senderName: "Wake Agent",
2161
- content: `wake message ${msgId}`,
2162
- requireReply: false,
2163
- timestamp: new Date().toISOString(),
2164
- };
2165
- }
2166
- it("returns todo block message when incomplete todos exist", async () => {
2167
- await writeTodoFile([
2168
- { content: "任务A", status: "pending", priority: "high" },
2169
- { content: "任务B", status: "completed", priority: "low" },
2170
- ]);
2171
- try {
2172
- const server = createServerForTodo();
2173
- const handleToolCall = getHandleToolCall(server);
2174
- // 有未完成 todo 时 poll_message 正常 JSON 返回提示消息(非异常)
2175
- const result = (await handleToolCall.call(server, "poll_message", {}));
2176
- assert.equal(result.directMessages.length, 1);
2177
- assert.equal(result.directMessages[0].senderId, "MCP-System");
2178
- assert.equal(result.directMessages[0].content, "当前有未处理的todo list不能使用poll_message阻塞获取消息");
2179
- }
2180
- finally {
2181
- await cleanupTodoFile();
2182
- }
2183
- });
2184
- it("returns todo block message even with unread messages", async () => {
2185
- await writeTodoFile([
2186
- { content: "任务A", status: "pending", priority: "high" },
2187
- ]);
2188
- try {
2189
- const server = createServerForTodo();
2190
- // 先把消息放入 buffer,模拟已有未读消息
2191
- const buffer = getMessageBuffer(server);
2192
- const msg = makeDirectMessage("msg-before-poll");
2193
- buffer.push(msg);
2194
- const handleToolCall = getHandleToolCall(server);
2195
- // todo 前置检查在 hydrate 之前,即使有未读消息也先返回提示
2196
- const result = (await handleToolCall.call(server, "poll_message", {}));
2197
- assert.equal(result.directMessages.length, 1);
2198
- assert.equal(result.directMessages[0].content, "当前有未处理的todo list不能使用poll_message阻塞获取消息");
2199
- }
2200
- finally {
2201
- await cleanupTodoFile();
2202
- }
2203
- });
2204
- it("allows poll_message when all todos are completed", async () => {
2205
- await writeTodoFile([
2206
- { content: "任务A", status: "completed", priority: "high" },
2207
- ]);
2208
- try {
2209
- const server = createServerForTodo();
2210
- const buffer = getMessageBuffer(server);
2211
- const wake = makeDirectMessage("wake-completed");
2212
- setTimeout(() => buffer.push(wake), 50);
2213
- const handleToolCall = getHandleToolCall(server);
2214
- const result = (await handleToolCall.call(server, "poll_message", {}));
2215
- // 全完成时无未完成 todo,进入 while 循环等待消息后返回
2216
- assert.equal(result.directMessages.length, 1);
2217
- assert.equal(result.directMessages[0].msgId, "wake-completed");
2218
- }
2219
- finally {
2220
- await cleanupTodoFile();
2221
- }
2222
- });
2223
- it("allows poll_message when todo file is absent", async () => {
2224
- // 确保文件不存在
2225
- await cleanupTodoFile();
2226
- const server = createServerForTodo();
2227
- const buffer = getMessageBuffer(server);
2228
- const wake = makeDirectMessage("wake-no-file");
2229
- setTimeout(() => buffer.push(wake), 50);
2230
- const handleToolCall = getHandleToolCall(server);
2231
- const result = (await handleToolCall.call(server, "poll_message", {}));
2232
- // 无 todo 文件 -> 进入 while 循环等待消息后返回
2233
- assert.equal(result.directMessages.length, 1);
2234
- assert.equal(result.directMessages[0].msgId, "wake-no-file");
2235
- });
2236
- it("returns todo block message when todo has in_progress status", async () => {
2237
- await writeTodoFile([
2238
- { content: "任务A", status: "in_progress", priority: "high" },
2239
- ]);
2240
- try {
2241
- const server = createServerForTodo();
2242
- const handleToolCall = getHandleToolCall(server);
2243
- const result = (await handleToolCall.call(server, "poll_message", {}));
2244
- assert.equal(result.directMessages.length, 1);
2245
- assert.equal(result.directMessages[0].content, "当前有未处理的todo list不能使用poll_message阻塞获取消息");
2246
- }
2247
- finally {
2248
- await cleanupTodoFile();
2249
- }
2250
- });
2251
- it("allows poll_message again after todos are all completed", async () => {
2252
- await writeTodoFile([
2253
- { content: "任务A", status: "pending", priority: "high" },
2254
- ]);
2255
- try {
2256
- const server = createServerForTodo();
2257
- const handleToolCall = getHandleToolCall(server);
2258
- // 第一次 poll:有未完成项 -> 返回提示消息(正常 JSON)
2259
- const result1 = (await handleToolCall.call(server, "poll_message", {}));
2260
- assert.equal(result1.directMessages.length, 1);
2261
- assert.equal(result1.directMessages[0].content, "当前有未处理的todo list不能使用poll_message阻塞获取消息");
2262
- // 全部完成后:poll_message 恢复正常阻塞,等消息返回
2263
- await writeTodoFile([
2264
- { content: "任务A", status: "completed", priority: "high" },
2265
- ]);
2266
- const buffer = getMessageBuffer(server);
2267
- const wake = makeDirectMessage("wake-after-complete");
2268
- setTimeout(() => buffer.push(wake), 50);
2269
- const result = (await handleToolCall.call(server, "poll_message", {}));
2270
- assert.equal(result.directMessages.length, 1);
2271
- assert.equal(result.directMessages[0].msgId, "wake-after-complete");
2272
- }
2273
- finally {
2274
- await cleanupTodoFile();
2275
- }
2276
- });
2277
- });
2278
- describe("McpServer meeting tools", () => {
2279
- const MEETING_TOOLS = [
2280
- "create_meeting",
2281
- "leave_meeting",
2282
- "end_meeting",
2283
- "meeting_speak",
2284
- "get_meeting_info",
2285
- "my_meetings",
2286
- ];
2287
- it("meeting_* 工具全部经 callAgentScopedTool 转发到服务端执行", async () => {
2288
- const calls = [];
2289
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2290
- callAgentScopedTool: async (name, args) => {
2291
- calls.push({ name, args });
2292
- return { status: "STARTED", meeting: { meetingId: "m-1" } };
2293
- },
2294
- });
2295
- const handleToolCall = Reflect.get(server, "handleToolCall");
2296
- for (const toolName of MEETING_TOOLS) {
2297
- const args = toolName === "meeting_speak"
2298
- ? { meetingId: "m-1", content: "大家好" }
2299
- : { meetingId: "m-1" };
2300
- await handleToolCall.call(server, toolName, args);
2301
- }
2302
- assert.equal(calls.length, MEETING_TOOLS.length);
2303
- assert.deepEqual(calls.map((c) => c.name), MEETING_TOOLS);
2304
- // 参数透传(agentId 由 agent-client 的 callAgentScopedTool 注入)
2305
- const speakCall = calls.find((c) => c.name === "meeting_speak");
2306
- assert.deepEqual(speakCall?.args, { meetingId: "m-1", content: "大家好" });
2307
- });
2308
- it("poll 结果中的会议通道消息原样交付(不打 _awsServerMessage,不参与 mark_dm_read 确认)", async () => {
2309
- const meetingMessage = {
2310
- msgId: "2001",
2311
- channel: "meeting",
2312
- meetingId: "m-1",
2313
- topic: "晨会",
2314
- senderId: "MEETING-SYSTEM",
2315
- senderName: "会议系统",
2316
- content: "会议已到位,共 3 人参会,可开始发言。",
2317
- isSystem: true,
2318
- timestamp: new Date().toISOString(),
2319
- };
2320
- const server = createServerWithUnread({
2321
- directMessages: [meetingMessage],
2322
- groupMessages: [],
2323
- });
2324
- const handleToolCall = Reflect.get(server, "handleToolCall");
2325
- const result = (await handleToolCall.call(server, "poll_message", {}));
2326
- assert.equal(result.directMessages.length, 1);
2327
- assert.equal(result.directMessages[0].channel, "meeting");
2328
- assert.equal(result.directMessages[0]
2329
- ._awsServerMessage, undefined, "会议消息不应带服务端确认标记(服务端 read_pos 已推进)");
2330
- });
2331
- it("会议邀请 SYSTEM_NOTIFY 唤醒阻塞 poll 后,强制 hydrate 同时取回邀请与自动加入提示", async () => {
2332
- const autoJoinNotice = {
2333
- msgId: 42,
2334
- channel: "meeting",
2335
- meetingId: "m-9",
2336
- topic: "评审会",
2337
- senderId: "MEETING-SYSTEM",
2338
- senderName: "会议系统",
2339
- content: "系统已自动为你加入会议「评审会」(会议ID:m-9)。",
2340
- isSystem: true,
2341
- timestamp: new Date().toISOString(),
2342
- };
2343
- const inviteNotice = {
2344
- msgId: "invite-9",
2345
- channel: "meeting_invite",
2346
- meetingId: "m-9",
2347
- topic: "评审会",
2348
- senderId: "MCP-System",
2349
- senderName: "MCP System",
2350
- content: "📢 [会议邀请] 你被邀请参加会议「评审会」(会议ID:m-9)。",
2351
- requireReply: false,
2352
- timestamp: new Date().toISOString(),
2353
- };
2354
- const markedNotifications = [];
2355
- let fetchCount = 0;
2356
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2357
- fetchUnreadMessages: async () => {
2358
- fetchCount += 1;
2359
- return fetchCount >= 3
2360
- ? { directMessages: [inviteNotice, autoJoinNotice], groupMessages: [] }
2361
- : { directMessages: [], groupMessages: [] };
2362
- },
2363
- markNotificationRead: async (msgIds) => {
2364
- markedNotifications.push(msgIds);
2365
- },
2366
- });
2367
- const handleToolCall = Reflect.get(server, "handleToolCall");
2368
- const pending = handleToolCall.call(server, "poll_message", {});
2369
- // 模拟服务端在 poll 阻塞期间推送会议邀请系统通知(WS SYSTEM_NOTIFY)
2370
- await new Promise((resolve) => setTimeout(resolve, 50));
2371
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2372
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "m-9" });
2373
- const result = (await Promise.race([
2374
- pending,
2375
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2376
- ]));
2377
- if (result === "timeout") {
2378
- assert.fail("会议邀请唤醒后 poll_message 必须立即返回,不得继续阻塞");
2379
- }
2380
- assert.equal(result.directMessages.length, 2);
2381
- assert.deepEqual(result.directMessages.map((message) => message.channel), ["meeting_invite", "meeting"]);
2382
- assert.equal(result.directMessages[0].msgId, "invite-9");
2383
- assert.equal(result.directMessages[1].msgId, 42);
2384
- assert.deepEqual(markedNotifications, [["invite-9"]]);
2385
- });
2386
- it("SYSTEM_NOTIFY 携带完整会议邀请(直接指名)时仅作唤醒信号:不入本地缓冲,由强制 hydrate 取回服务端抢占结果", async () => {
2387
- let fetchCount = 0;
2388
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2389
- fetchUnreadMessages: async () => {
2390
- fetchCount += 1;
2391
- // 第 1 次 = 入口 hydrate;第 2 次 = 邀请唤醒的强制 hydrate。
2392
- // 模拟服务端 poll:抢占式入会返回自动加入提示,未读邀请通知随白名单一并交付
2393
- return fetchCount >= 2
2394
- ? {
2395
- directMessages: [
2396
- {
2397
- msgId: "invite-direct-1",
2398
- channel: "meeting_invite",
2399
- senderId: "MCP-System",
2400
- content: "📢 [会议邀请] 你被邀请参加会议「晨会」。",
2401
- requireReply: false,
2402
- timestamp: new Date().toISOString(),
2403
- },
2404
- {
2405
- msgId: "auto-join-1",
2406
- channel: "meeting",
2407
- meetingId: "meeting-direct-1",
2408
- senderId: "MCP-System",
2409
- content: "系统已自动为你加入会议「晨会」。",
2410
- isSystem: true,
2411
- timestamp: new Date().toISOString(),
2412
- },
2413
- ],
2414
- groupMessages: [],
2415
- }
2416
- : { directMessages: [], groupMessages: [] };
2417
- },
2418
- });
2419
- const handleToolCall = Reflect.get(server, "handleToolCall");
2420
- const pending = handleToolCall.call(server, "poll_message", {});
2421
- await new Promise((resolve) => setTimeout(resolve, 20));
2422
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2423
- handleSystemNotify.call(server, {
2424
- id: "invite-direct-1",
2425
- type: "meeting_invite",
2426
- senderId: "MCP-System",
2427
- senderName: "MCP System",
2428
- content: "📢 [会议邀请] 你被邀请参加会议「晨会」(会议ID:meeting-direct-1)。",
2429
- timestamp: new Date().toISOString(),
2430
- payload: JSON.stringify({ meetingId: "meeting-direct-1" }),
2431
- });
2432
- const result = (await Promise.race([
2433
- pending,
2434
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2435
- ]));
2436
- assert.notEqual(result, "timeout");
2437
- if (result === "timeout")
2438
- return;
2439
- // 携带 id 的直接指名邀请同样不入 WS 缓冲:内容全部来自强制 hydrate 的服务端结果
2440
- assert.equal(result.directMessages.length, 2);
2441
- assert.equal(result.directMessages[0].msgId, "invite-direct-1");
2442
- assert.equal(result.directMessages[1].msgId, "auto-join-1");
2443
- assert.ok(fetchCount >= 2);
2444
- });
2445
- it("竞争性会议邀请(角色席位 competitive=true)仅作唤醒信号:不入本地缓冲,交付服务端抢占后的自动加入提示", async () => {
2446
- let fetchCount = 0;
2447
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2448
- fetchUnreadMessages: async () => {
2449
- fetchCount += 1;
2450
- // 第 1 次 = 入口 hydrate(无内容);第 2 次 = 会议邀请强制 hydrate,
2451
- // 模拟服务端抢占式入会成功后返回"已自动加入会议"系统提示
2452
- return fetchCount >= 2
2453
- ? {
2454
- directMessages: [
2455
- {
2456
- msgId: "auto-join-1",
2457
- channel: "meeting",
2458
- meetingId: "meeting-comp-1",
2459
- senderId: "MCP-System",
2460
- content: "系统已自动为你加入会议「晨会」(会议ID:meeting-comp-1)。",
2461
- requireReply: false,
2462
- timestamp: new Date().toISOString(),
2463
- },
2464
- ],
2465
- groupMessages: [],
2466
- }
2467
- : { directMessages: [], groupMessages: [] };
2468
- },
2469
- });
2470
- const handleToolCall = Reflect.get(server, "handleToolCall");
2471
- const pending = handleToolCall.call(server, "poll_message", {});
2472
- await new Promise((resolve) => setTimeout(resolve, 20));
2473
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2474
- handleSystemNotify.call(server, {
2475
- id: "invite-comp-1",
2476
- type: "meeting_invite",
2477
- senderId: "MCP-System",
2478
- senderName: "MCP System",
2479
- content: "📢 [会议邀请] 角色「共享角色」被邀请参加会议「晨会」。",
2480
- timestamp: new Date().toISOString(),
2481
- payload: JSON.stringify({ meetingId: "meeting-comp-1", roleId: "role-shared", competitive: true }),
2482
- });
2483
- const result = (await Promise.race([
2484
- pending,
2485
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2486
- ]));
2487
- assert.notEqual(result, "timeout", "竞争性邀请应立即唤醒 poll 并经强制 hydrate 返回抢占结果");
2488
- if (result === "timeout")
2489
- return;
2490
- // 邀请文本本身不进本地缓冲;交付的是服务端抢占入会后的自动加入提示
2491
- assert.equal(result.directMessages.length, 1);
2492
- assert.equal(result.directMessages[0].msgId, "auto-join-1");
2493
- assert.ok(fetchCount >= 2, "应触发会议邀请强制 hydrate 完成服务端抢占");
2494
- });
2495
- it("会议消息(channel=meeting)交付前经 mark_meeting_read 确认推进服务端游标", async () => {
2496
- let fetchCount = 0;
2497
- const confirmedIds = [];
2498
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2499
- fetchUnreadMessages: async () => {
2500
- fetchCount += 1;
2501
- return fetchCount >= 2
2502
- ? {
2503
- directMessages: [
2504
- {
2505
- msgId: "auto-join-confirm-1",
2506
- channel: "meeting",
2507
- meetingId: "meeting-confirm-1",
2508
- senderId: "MCP-System",
2509
- content: "系统已自动为你加入会议「晨会」。",
2510
- isSystem: true,
2511
- timestamp: new Date().toISOString(),
2512
- },
2513
- ],
2514
- groupMessages: [],
2515
- }
2516
- : { directMessages: [], groupMessages: [] };
2517
- },
2518
- markMeetingRead: async (msgIds) => {
2519
- confirmedIds.push(...msgIds);
2520
- },
2521
- });
2522
- const handleToolCall = Reflect.get(server, "handleToolCall");
2523
- const pending = handleToolCall.call(server, "poll_message", {});
2524
- await new Promise((resolve) => setTimeout(resolve, 20));
2525
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2526
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "meeting-confirm-1" });
2527
- const result = (await Promise.race([
2528
- pending,
2529
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2530
- ]));
2531
- assert.notEqual(result, "timeout");
2532
- if (result === "timeout")
2533
- return;
2534
- assert.equal(result.directMessages.length, 1);
2535
- assert.equal(result.directMessages[0].msgId, "auto-join-confirm-1");
2536
- // 服务端 peek 不推进 read_pos:交付成功必须经 mark_meeting_read 确认,
2537
- // 否则客户端超时丢响应后「已自动加入/会议开始」永久丢失、poll_message 永不返回
2538
- assert.deepEqual(confirmedIds, ["auto-join-confirm-1"]);
2539
- });
2540
- it("mark_meeting_read 确认失败时降级交付:会议消息不因确认异常丢失", async () => {
2541
- let fetchCount = 0;
2542
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2543
- fetchUnreadMessages: async () => {
2544
- fetchCount += 1;
2545
- return fetchCount >= 2
2546
- ? {
2547
- directMessages: [
2548
- {
2549
- msgId: "auto-join-degrade-1",
2550
- channel: "meeting",
2551
- meetingId: "meeting-degrade-1",
2552
- senderId: "MCP-System",
2553
- content: "系统已自动为你加入会议「晨会」。",
2554
- isSystem: true,
2555
- timestamp: new Date().toISOString(),
2556
- },
2557
- ],
2558
- groupMessages: [],
2559
- }
2560
- : { directMessages: [], groupMessages: [] };
2561
- },
2562
- markMeetingRead: async () => {
2563
- throw new Error("mark_meeting_read 服务端暂不可用");
2564
- },
2565
- });
2566
- const handleToolCall = Reflect.get(server, "handleToolCall");
2567
- const pending = handleToolCall.call(server, "poll_message", {});
2568
- await new Promise((resolve) => setTimeout(resolve, 20));
2569
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2570
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "meeting-degrade-1" });
2571
- const result = (await Promise.race([
2572
- pending,
2573
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2574
- ]));
2575
- assert.notEqual(result, "timeout", "确认失败必须降级为直接交付,poll 不能死等");
2576
- if (result === "timeout")
2577
- return;
2578
- assert.equal(result.directMessages.length, 1);
2579
- assert.equal(result.directMessages[0].msgId, "auto-join-degrade-1");
2580
- });
2581
- it("服务端 peek 重投同一会议消息时本地去重拦截,不二次交付给 LLM", async () => {
2582
- const meetingMessage = {
2583
- msgId: "auto-join-redeliver-1",
2584
- channel: "meeting",
2585
- meetingId: "meeting-redeliver-1",
2586
- senderId: "MCP-System",
2587
- content: "系统已自动为你加入会议「晨会」。",
2588
- isSystem: true,
2589
- timestamp: new Date().toISOString(),
2590
- };
2591
- let fetchCount = 0;
2592
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2593
- // 服务端为 peek 语义:客户端确认前(或确认丢失时)每次 hydrate 都重投同一条
2594
- fetchUnreadMessages: async () => {
2595
- fetchCount += 1;
2596
- return fetchCount >= 2
2597
- ? { directMessages: [meetingMessage], groupMessages: [] }
2598
- : { directMessages: [], groupMessages: [] };
2599
- },
2600
- });
2601
- const handleToolCall = Reflect.get(server, "handleToolCall");
2602
- const pending = handleToolCall.call(server, "poll_message", {});
2603
- await new Promise((resolve) => setTimeout(resolve, 20));
2604
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2605
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "meeting-redeliver-1" });
2606
- const result = (await Promise.race([
2607
- pending,
2608
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2609
- ]));
2610
- assert.notEqual(result, "timeout");
2611
- if (result === "timeout")
2612
- return;
2613
- // 强制 hydrate 循环内同一消息可能被 peek 多次重投,交付结果必须只含一条
2614
- assert.equal(result.directMessages.length, 1);
2615
- assert.equal(result.directMessages[0].msgId, "auto-join-redeliver-1");
2616
- });
2617
- it("会议邀请唤醒的强制 hydrate 携带 preemptiveJoinWaitMs 预算与覆盖服务端等待的请求超时", async () => {
2618
- const fetchOptions = [];
2619
- let fetchCount = 0;
2620
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2621
- fetchUnreadMessages: async (options) => {
2622
- fetchOptions.push(options);
2623
- fetchCount += 1;
2624
- return fetchCount >= 2
2625
- ? {
2626
- directMessages: [
2627
- {
2628
- msgId: "auto-join-wait-1",
2629
- channel: "meeting",
2630
- meetingId: "meeting-wait-9",
2631
- senderId: "MCP-System",
2632
- content: "系统已自动为你加入会议「晨会」(会议ID:meeting-wait-9)。",
2633
- isSystem: true,
2634
- timestamp: new Date().toISOString(),
2635
- },
2636
- ],
2637
- groupMessages: [],
2638
- }
2639
- : { directMessages: [], groupMessages: [] };
2640
- },
2641
- });
2642
- const handleToolCall = Reflect.get(server, "handleToolCall");
2643
- const pending = handleToolCall.call(server, "poll_message", {});
2644
- await new Promise((resolve) => setTimeout(resolve, 20));
2645
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2646
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "meeting-wait-9" });
2647
- const result = (await Promise.race([
2648
- pending,
2649
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2650
- ]));
2651
- assert.notEqual(result, "timeout");
2652
- if (result === "timeout")
2653
- return;
2654
- assert.ok(fetchOptions.length >= 2, "入口 hydrate + 强制 hydrate 均应发生");
2655
- // 入口 hydrate 显式传 0("立即加入但不阻塞等待"):省略会让服务端回退默认阻塞预算,
2656
- // 与入口的短客户端超时错配;等待全员到位由强制 hydrate 的预算完成
2657
- assert.equal(fetchOptions[0]?.preemptiveJoinWaitMs, 0);
2658
- assert.notEqual(fetchOptions[0]?.propagatePollTimeout, true);
2659
- // 强制 hydrate 携带完整等待预算,单请求超时必须覆盖服务端等待预算,
2660
- // 否则合法长请求被误判超时
2661
- assert.equal(fetchOptions[1]?.preemptiveJoinWaitMs, 45_000);
2662
- assert.equal(fetchOptions[1]?.pollMessageTimeoutMs, 65_000);
2663
- assert.equal(fetchOptions[1]?.skipGroupMessages, true);
2664
- assert.equal(fetchOptions[1]?.propagatePollTimeout, true);
2665
- assert.equal(result.directMessages[0].msgId, "auto-join-wait-1");
2666
- });
2667
- it("会议邀请强制 hydrate 客户端超时:不立即连发重试,稍后经自愈重挂继续抢占", async () => {
2668
- const fetchTimestamps = [];
2669
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2670
- fetchUnreadMessages: async (options) => {
2671
- fetchTimestamps.push(Date.now());
2672
- const call = fetchTimestamps.length;
2673
- // 第 1 次 = 入口 hydrate;第 2 次 = 强制 hydrate,模拟客户端硬超时
2674
- if (call === 2 && options?.propagatePollTimeout === true) {
2675
- throw new PollLongPollTimeoutError("MCP tool poll_message timed out after 65000ms");
2676
- }
2677
- // 自愈重挂后的强制 hydrate 仍返回空,第 2 次重试交付自动加入提示,
2678
- // 使 pending poll 自然结束(不遗留活跃定时器阻塞测试进程退出)
2679
- if (call >= 4) {
2680
- return {
2681
- directMessages: [
2682
- {
2683
- msgId: "auto-join-rearm-1",
2684
- channel: "meeting",
2685
- meetingId: "meeting-timeout-1",
2686
- senderId: "MCP-System",
2687
- content: "系统已自动为你加入会议「晨会」(会议ID:meeting-timeout-1)。",
2688
- isSystem: true,
2689
- timestamp: new Date().toISOString(),
2690
- },
2691
- ],
2692
- groupMessages: [],
2693
- };
2694
- }
2695
- return { directMessages: [], groupMessages: [] };
2696
- },
2697
- });
2698
- // 注入短自愈重挂延迟(默认 10s,测试无法等待)
2699
- Reflect.set(server, "meetingHydrateTimeoutRearmDelayMs", 50);
2700
- const handleToolCall = Reflect.get(server, "handleToolCall");
2701
- const pending = handleToolCall.call(server, "poll_message", {});
2702
- await new Promise((resolve) => setTimeout(resolve, 20));
2703
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2704
- handleSystemNotify.call(server, { type: "meeting_invite", meetingId: "meeting-timeout-1" });
2705
- const result = (await Promise.race([
2706
- pending,
2707
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 3000)),
2708
- ]));
2709
- assert.notEqual(result, "timeout", "自愈重挂后应继续强制 hydrate 并交付抢占结果");
2710
- if (result === "timeout")
2711
- return;
2712
- assert.ok(fetchTimestamps.length >= 3, "超时后应经自愈重挂再次发起强制 hydrate");
2713
- // 关键断言:超时后没有 150ms 间隔的立即重试——第 3 次请求必须等自愈重挂延迟
2714
- assert.ok(fetchTimestamps[2] - fetchTimestamps[1] >= 45, "客户端超时后不应立即重试(服务端可能仍在执行上一请求),应等待自愈重挂延迟");
2715
- assert.equal(result.directMessages[0].msgId, "auto-join-rearm-1");
2716
- });
2717
- it("meeting_creator_prompt 系统通知同样唤醒受阻 poll:全员聆听时创建者请发言提醒即时送达", async () => {
2718
- let fetchCount = 0;
2719
- const server = createServerWithUnread({ directMessages: [], groupMessages: [] }, {
2720
- fetchUnreadMessages: async () => {
2721
- fetchCount += 1;
2722
- // 第 1 次 = 入口 hydrate(无内容);第 2 次 = 创建者提醒唤醒的强制 hydrate,
2723
- // 会议模式白名单取回 meeting_creator_prompt 定向提醒
2724
- return fetchCount >= 2
2725
- ? {
2726
- directMessages: [
2727
- {
2728
- msgId: "creator-prompt-1",
2729
- channel: "notification",
2730
- senderId: "MCP-System",
2731
- content: "🔔 [会议提醒] 「晨会」当前没有人发言,你作为会议创建者,请你进行会议发言。",
2732
- requireReply: false,
2733
- timestamp: new Date().toISOString(),
2734
- },
2735
- ],
2736
- groupMessages: [],
2737
- }
2738
- : { directMessages: [], groupMessages: [] };
2739
- },
2740
- });
2741
- const handleToolCall = Reflect.get(server, "handleToolCall");
2742
- const pending = handleToolCall.call(server, "poll_message", {});
2743
- await new Promise((resolve) => setTimeout(resolve, 20));
2744
- const handleSystemNotify = Reflect.get(server, "handleSystemNotify");
2745
- handleSystemNotify.call(server, {
2746
- id: "creator-prompt-1",
2747
- type: "meeting_creator_prompt",
2748
- senderId: "MCP-System",
2749
- senderName: "MCP System",
2750
- content: "🔔 [会议提醒] 「晨会」当前没有人发言,你作为会议创建者,请你进行会议发言。",
2751
- timestamp: new Date().toISOString(),
2752
- payload: JSON.stringify({ meetingId: "meeting-silent-1", role: "creator" }),
2753
- });
2754
- const result = (await Promise.race([
2755
- pending,
2756
- new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
2757
- ]));
2758
- assert.notEqual(result, "timeout", "创建者提醒应立即唤醒 poll 而非等 30s hydrate 周期");
2759
- if (result === "timeout")
2760
- return;
2761
- assert.ok(fetchCount >= 2);
2762
- assert.equal(result.directMessages.length, 1);
2763
- assert.equal(result.directMessages[0].msgId, "creator-prompt-1");
2764
- assert.ok(String(result.directMessages[0].content).includes("请你进行会议发言"));
2765
- });
2766
- it("meeting_invite 通知 DM 不打 _awsServerMessage,确认查不到 t_direct_message 行也不被丢弃", async () => {
2767
- const inviteNotice = {
2768
- msgId: "notif-uuid-001",
2769
- channel: "meeting_invite",
2770
- meetingId: "m-9",
2771
- topic: "评审会",
2772
- senderId: "MCP-System",
2773
- senderName: "系统",
2774
- content: "📢 [会议邀请] 你被邀请参加会议「评审会」(会议ID:m-9)。",
2775
- requireReply: false,
2776
- timestamp: new Date().toISOString(),
2777
- };
2778
- // markDmRead 模拟服务端真实行为:通知 UUID 不在 t_direct_message,返回空集合。
2779
- // 若消息被误打 _awsServerMessage 标记,confirmServerMessages 会将其当作"已撤回"丢弃,
2780
- // 同时服务端通知已"取即已读" → 邀请永久丢失,poll_message 永远等不到。
2781
- let markDmReadCalled = false;
2782
- const server = createServerWithUnread({ directMessages: [inviteNotice], groupMessages: [] }, {
2783
- markDmRead: async () => {
2784
- markDmReadCalled = true;
2785
- return new Set();
2786
- },
2787
- });
2788
- const handleToolCall = Reflect.get(server, "handleToolCall");
2789
- const result = (await handleToolCall.call(server, "poll_message", {}));
2790
- // 入口 hydrate 拉到的 meeting_invite 通知必须交付(即使 mark_dm_read 确认不到)
2791
- assert.equal(result.directMessages.length, 1);
2792
- assert.equal(result.directMessages[0].channel, "meeting_invite");
2793
- assert.equal(result.directMessages[0].msgId, "notif-uuid-001");
2794
- assert.equal(result.directMessages[0]
2795
- ._awsServerMessage, undefined, "meeting_invite 通知 DM 不应带服务端确认标记(UUID 不在 t_direct_message,确认必失败)");
2796
- assert.equal(markDmReadCalled, false, "meeting_invite 通知 DM 不应触发 mark_dm_read 确认(不在 t_direct_message)");
2797
- });
2798
- });