





















@@ -137,7 +137,7 @@ vi.mock("../agents/subagent-control.js", () => ({
137137138138vi.mock("../utils/message-channel.js", () => ({
139139isDeliverableMessageChannel: (channel: string) =>
140-channel === "notifychat" || channel === "guildchat",
140+channel === "notifychat" || channel === "guildchat" || channel === "discord",
141141}));
142142143143function configureTaskRegistryMaintenanceRuntimeForTest(params: {
@@ -1323,6 +1323,148 @@ describe("task-registry", () => {
13231323});
13241324});
132513251326+it("delivers delegated ACP completion directly to an explicitly bound Discord thread", async () => {
1327+await withTaskRegistryTempDir(async (root) => {
1328+process.env.OPENCLAW_STATE_DIR = root;
1329+resetTaskRegistryForTests();
1330+const runId = "run-bound-discord-thread-terminal";
1331+hoisted.sendMessageMock.mockResolvedValue({
1332+channel: "discord",
1333+to: "channel:parent-channel",
1334+via: "direct",
1335+});
1336+1337+createTaskRecord({
1338+runtime: "acp",
1339+ownerKey: "agent:main:discord:guild-123:channel-parent-channel",
1340+scopeKind: "session",
1341+requesterOrigin: {
1342+channel: "discord",
1343+to: "channel:parent-channel",
1344+threadId: "thread-84022",
1345+},
1346+childSessionKey: "agent:main:acp:child",
1347+ runId,
1348+task: "Investigate thread-bound ACP delivery",
1349+status: "running",
1350+deliveryStatus: "pending",
1351+terminalSummary: "ACP final answer",
1352+startedAt: 100,
1353+});
1354+1355+emitAgentEvent({
1356+ runId,
1357+stream: "lifecycle",
1358+data: {
1359+phase: "end",
1360+endedAt: 250,
1361+},
1362+});
1363+1364+await waitForAssertion(() => {
1365+const task = findTaskByRunId(runId);
1366+if (!task) {
1367+throw new Error(`Expected task for run ${runId}`);
1368+}
1369+expect(task.status).toBe("succeeded");
1370+expect(task.deliveryStatus).toBe("delivered");
1371+});
1372+await waitForAssertion(() => expect(hoisted.sendMessageMock).toHaveBeenCalledTimes(1));
1373+const message = sentMessageCall();
1374+expectRecordFields(message, {
1375+channel: "discord",
1376+to: "channel:parent-channel",
1377+threadId: "thread-84022",
1378+});
1379+expect(String(message.content)).toContain(
1380+"Background task ready for review: ACP background task",
1381+);
1382+expect(String(message.content)).toContain("ACP final answer");
1383+expect(String(message.content)).toContain(
1384+"Next: parent will review/verify before calling it done.",
1385+);
1386+expect(peekSystemEvents("agent:main:discord:guild-123:channel-parent-channel")).toStrictEqual(
1387+[],
1388+);
1389+});
1390+});
1391+1392+it.each([
1393+{
1394+id: "missing-thread",
1395+requesterOrigin: {
1396+channel: "discord",
1397+to: "channel:parent-channel",
1398+},
1399+},
1400+{
1401+id: "non-channel-target",
1402+requesterOrigin: {
1403+channel: "discord",
1404+to: "user:U123",
1405+threadId: "thread-84022",
1406+},
1407+},
1408+{
1409+id: "non-discord-channel",
1410+requesterOrigin: {
1411+channel: "guildchat",
1412+to: "guildchat:channel:parent-channel",
1413+threadId: "thread-84022",
1414+},
1415+},
1416+])(
1417+"keeps delegated ACP completion queued without an explicit bound Discord thread ($id)",
1418+async ({ requesterOrigin }) => {
1419+await withTaskRegistryTempDir(async (root) => {
1420+process.env.OPENCLAW_STATE_DIR = root;
1421+resetTaskRegistryForTests();
1422+const runId = `run-non-bound-discord-thread-terminal-${requesterOrigin.channel}-${requesterOrigin.to}`;
1423+hoisted.sendMessageMock.mockResolvedValue({
1424+channel: requesterOrigin.channel,
1425+to: requesterOrigin.to,
1426+via: "direct",
1427+});
1428+1429+createTaskRecord({
1430+runtime: "acp",
1431+ownerKey: "agent:main:discord:guild-123:channel-parent-channel",
1432+scopeKind: "session",
1433+ requesterOrigin,
1434+childSessionKey: "agent:main:acp:child",
1435+ runId,
1436+task: "Investigate thread-bound ACP delivery",
1437+status: "running",
1438+deliveryStatus: "pending",
1439+terminalSummary: "ACP final answer",
1440+startedAt: 100,
1441+});
1442+1443+emitAgentEvent({
1444+ runId,
1445+stream: "lifecycle",
1446+data: {
1447+phase: "end",
1448+endedAt: 250,
1449+},
1450+});
1451+1452+await waitForAssertion(() => {
1453+const task = findTaskByRunId(runId);
1454+if (!task) {
1455+throw new Error(`Expected task for run ${runId}`);
1456+}
1457+expect(task.status).toBe("succeeded");
1458+expect(task.deliveryStatus).toBe("session_queued");
1459+});
1460+expect(hoisted.sendMessageMock).not.toHaveBeenCalled();
1461+expect(peekSystemEvents("agent:main:discord:guild-123:channel-parent-channel")).toEqual([
1462+expect.stringContaining("Background task ready for review: ACP background task"),
1463+]);
1464+});
1465+},
1466+);
1467+13261468it.each([
13271469{
13281470id: "channel",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。