aws-runtime-bridge 1.9.124 → 1.9.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/adapter/AcodeSdkAdapter.js +4 -4
- package/dist/adapter/OpencodeSdkAdapter.js +2 -2
- package/dist/adapter/types.js +1 -1
- package/dist/config.d.ts +30 -4
- package/dist/config.js +3 -1
- package/dist/index.js +1 -1
- package/dist/routes/acode-todos.js +1 -1
- package/dist/routes/background-tasks.js +1 -1
- package/dist/routes/dashboard.js +58 -3
- package/dist/routes/git.d.ts +28 -0
- package/dist/routes/git.js +32 -31
- package/dist/routes/terminal.js +24 -24
- package/dist/services/auto-register.d.ts +2 -2
- package/dist/services/auto-register.js +1 -1
- package/dist/services/aws-client-agent-mcp.d.ts +2 -1
- package/dist/services/aws-client-agent-mcp.js +2 -2
- package/dist/services/cc-switch-sdk.js +1 -1
- package/dist/services/cli-commands.js +5 -5
- package/dist/services/fileSnapshotStore.js +1 -1
- package/dist/services/instance-ai-config-service.js +1 -1
- package/dist/services/instance-features.d.ts +2 -0
- package/dist/services/instance-features.js +1 -1
- package/dist/services/panel-auth.d.ts +2 -0
- package/dist/services/panel-auth.js +1 -1
- package/dist/services/startup-config-wizard.js +6 -6
- package/dist/services/tunnel-client.js +1 -1
- package/dist/services/update-notifier.js +2 -2
- package/package/acode/dist/ask-user-question-tool.js +2 -2
- package/package/acode/dist/background-command-store.js +1 -1
- package/package/acode/dist/builtins/commandArtifacts.js +3 -3
- package/package/acode/dist/mcpToolNames.d.ts +2 -0
- package/package/acode/dist/mcpToolNames.js +1 -1
- package/package/acode/dist/meeting-debug-log.d.ts +1 -0
- package/package/acode/dist/meeting-debug-log.js +3 -0
- package/package/acode/dist/paths.d.ts +2 -0
- package/package/acode/dist/paths.js +2 -0
- package/package/acode/dist/runtime.d.ts +4 -0
- package/package/acode/dist/runtime.js +20 -20
- package/package/acode/dist/sub-agent-manager.d.ts +1 -0
- package/package/acode/dist/sub-agent-manager.js +1 -1
- package/package/acode/dist/sub-agent-store.js +3 -3
- package/package/acode/dist/sub-agent-tools.js +27 -31
- package/package/acode/dist/sub-agent-types.d.ts +1 -1
- package/package/acode/dist/todo-store.js +1 -1
- package/package/acode/dist/tool-allocation.d.ts +18 -4
- package/package/acode/dist/tool-allocation.js +1 -1
- package/package/acode/dist/types.d.ts +0 -3
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +17 -0
- package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +251 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/config.js +1 -1
- package/package/aws-client-agent-mcp/dist/config.test.js +93 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +6 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -28
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +628 -7
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +10 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +12 -10
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +38 -0
- package/package/aws-client-agent-mcp/dist/meeting-debug-log.d.ts +13 -0
- package/package/aws-client-agent-mcp/dist/meeting-debug-log.js +3 -0
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/message-buffer.js +1 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.test.js +60 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts +14 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +39 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +11 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
- package/package/aws-client-agent-mcp/package.json +1 -1
- package/package.json +1 -1
|
@@ -2,8 +2,8 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { describe, it } from "node:test";
|
|
6
|
-
import { AgentClient } from "./agent-client.js";
|
|
5
|
+
import { after, before, describe, it } from "node:test";
|
|
6
|
+
import { AgentClient, PollLongPollTimeoutError } from "./agent-client.js";
|
|
7
7
|
import { McpServer } from "./mcp-server.js";
|
|
8
8
|
import { MCP_TOOLS } from "./mcp-tools.js";
|
|
9
9
|
import { MemoryStore, } from "./memory-store.js";
|
|
@@ -21,6 +21,10 @@ function createServerWithUnread(unreadMessages, overrides = {}) {
|
|
|
21
21
|
isRegistered: true,
|
|
22
22
|
}),
|
|
23
23
|
fetchUnreadMessages: async () => unreadMessages,
|
|
24
|
+
markMeetingRead: async () => { },
|
|
25
|
+
callAgentScopedTool: async () => {
|
|
26
|
+
throw new Error("unexpected callAgentScopedTool in test stub");
|
|
27
|
+
},
|
|
24
28
|
...overrides,
|
|
25
29
|
};
|
|
26
30
|
Reflect.set(server, "agentClient", agentClientStub);
|
|
@@ -194,6 +198,65 @@ describe("McpServer unread backlog hydration", () => {
|
|
|
194
198
|
assert.equal(fetchOptions[0].blockIfEmpty, false);
|
|
195
199
|
assert.equal(fetchOptions[0].includePipelineTasks, true);
|
|
196
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
|
+
});
|
|
197
260
|
it("poll_message should handle hydration error gracefully and receive WebSocket messages", async () => {
|
|
198
261
|
const directMessage = {
|
|
199
262
|
msgId: "dm-hydration-error-ws",
|
|
@@ -1635,6 +1698,8 @@ describe("McpServer group discovery tool", () => {
|
|
|
1635
1698
|
capturedArgs = args;
|
|
1636
1699
|
return hiredResult;
|
|
1637
1700
|
},
|
|
1701
|
+
// 「招聘工具」能力开启,通过执行期守卫(默认关闭)
|
|
1702
|
+
getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
|
|
1638
1703
|
getStatusReporter: () => null,
|
|
1639
1704
|
});
|
|
1640
1705
|
const handleToolCall = Reflect.get(server, "handleToolCall");
|
|
@@ -1660,6 +1725,11 @@ describe("McpServer group discovery tool", () => {
|
|
|
1660
1725
|
});
|
|
1661
1726
|
it("hire_colleague should validate required workspacePath", async () => {
|
|
1662
1727
|
const server = new McpServer();
|
|
1728
|
+
// 「招聘工具」能力开启,通过执行期守卫(默认关闭)
|
|
1729
|
+
Reflect.set(server, "agentClient", {
|
|
1730
|
+
getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
|
|
1731
|
+
getStatusReporter: () => null,
|
|
1732
|
+
});
|
|
1663
1733
|
const handleToolCall = Reflect.get(server, "handleToolCall");
|
|
1664
1734
|
await assert.rejects(async () => {
|
|
1665
1735
|
await handleToolCall.call(server, "hire_colleague", {
|
|
@@ -1704,6 +1774,8 @@ describe("McpServer group discovery tool", () => {
|
|
|
1704
1774
|
capturedArgs = args;
|
|
1705
1775
|
return batchResult;
|
|
1706
1776
|
},
|
|
1777
|
+
// 「招聘工具」能力开启,通过执行期守卫(默认关闭)
|
|
1778
|
+
getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
|
|
1707
1779
|
getStatusReporter: () => null,
|
|
1708
1780
|
});
|
|
1709
1781
|
const handleToolCall = Reflect.get(server, "handleToolCall");
|
|
@@ -1745,6 +1817,11 @@ describe("McpServer group discovery tool", () => {
|
|
|
1745
1817
|
});
|
|
1746
1818
|
it("hire_colleagues_batch should validate required items", async () => {
|
|
1747
1819
|
const server = new McpServer();
|
|
1820
|
+
// 「招聘工具」能力开启,通过执行期守卫(默认关闭)
|
|
1821
|
+
Reflect.set(server, "agentClient", {
|
|
1822
|
+
getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
|
|
1823
|
+
getStatusReporter: () => null,
|
|
1824
|
+
});
|
|
1748
1825
|
const handleToolCall = Reflect.get(server, "handleToolCall");
|
|
1749
1826
|
await assert.rejects(async () => {
|
|
1750
1827
|
await handleToolCall.call(server, "hire_colleagues_batch", {
|
|
@@ -1755,6 +1832,11 @@ describe("McpServer group discovery tool", () => {
|
|
|
1755
1832
|
});
|
|
1756
1833
|
it("hire_colleagues_batch should validate every item workspacePath", async () => {
|
|
1757
1834
|
const server = new McpServer();
|
|
1835
|
+
// 「招聘工具」能力开启,通过执行期守卫(默认关闭)
|
|
1836
|
+
Reflect.set(server, "agentClient", {
|
|
1837
|
+
getRuntimeFeatures: async () => ({ "agentSys.hire": true }),
|
|
1838
|
+
getStatusReporter: () => null,
|
|
1839
|
+
});
|
|
1758
1840
|
const handleToolCall = Reflect.get(server, "handleToolCall");
|
|
1759
1841
|
await assert.rejects(async () => {
|
|
1760
1842
|
await handleToolCall.call(server, "hire_colleagues_batch", {
|
|
@@ -2027,13 +2109,31 @@ describe("AgentClient group room aggregation", () => {
|
|
|
2027
2109
|
});
|
|
2028
2110
|
});
|
|
2029
2111
|
describe("McpServer todo guard on poll_message", () => {
|
|
2030
|
-
// 使用唯一 agentId 避免污染开发者真实 todo
|
|
2112
|
+
// 使用唯一 agentId 避免污染开发者真实 todo 文件;每次测试写入并清理。
|
|
2113
|
+
// ACODE_HOME 指向临时目录:与被测的 resolveAcodeDataHome() 同源,不触碰真实 ~/.acode
|
|
2031
2114
|
const testAgentId = `test-todo-reminder-${process.pid}-${Date.now()}`;
|
|
2032
|
-
|
|
2033
|
-
|
|
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`);
|
|
2034
2134
|
async function writeTodoFile(todos) {
|
|
2035
2135
|
await mkdir(todoDir, { recursive: true });
|
|
2036
|
-
await writeFile(todoPath, JSON.stringify({
|
|
2136
|
+
await writeFile(todoPath(), JSON.stringify({
|
|
2037
2137
|
agentId: testAgentId,
|
|
2038
2138
|
updatedAt: new Date().toISOString(),
|
|
2039
2139
|
background: "test background",
|
|
@@ -2042,7 +2142,7 @@ describe("McpServer todo guard on poll_message", () => {
|
|
|
2042
2142
|
}
|
|
2043
2143
|
async function cleanupTodoFile() {
|
|
2044
2144
|
try {
|
|
2045
|
-
await rm(todoPath, { force: true });
|
|
2145
|
+
await rm(todoPath(), { force: true });
|
|
2046
2146
|
}
|
|
2047
2147
|
catch {
|
|
2048
2148
|
// 忽略清理失败
|
|
@@ -2175,3 +2275,524 @@ describe("McpServer todo guard on poll_message", () => {
|
|
|
2175
2275
|
}
|
|
2176
2276
|
});
|
|
2177
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
|
+
});
|