Skip to content

Commit 95c9055

Browse files
committed
fix(feishu): fallback after failed visible final sends
1 parent 3450a68 commit 95c9055

3 files changed

Lines changed: 106 additions & 8 deletions

File tree

extensions/feishu/src/bot.broadcast.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,48 @@ describe("broadcast dispatch", () => {
381381
});
382382

383383
expect(ensureNoVisibleReplyFallback).toHaveBeenCalledWith(
384-
"broadcast-dispatch-complete-zero-final",
384+
"broadcast-dispatch-complete-no-visible-reply",
385+
);
386+
});
387+
388+
it("sends no-visible-reply fallback for active broadcast failed final delivery", async () => {
389+
mockDispatchReplyFromConfig
390+
.mockResolvedValueOnce({ queuedFinal: false, counts: { final: 1 } })
391+
.mockResolvedValueOnce({
392+
queuedFinal: true,
393+
counts: { final: 1 },
394+
});
395+
const ensureNoVisibleReplyFallback = vi.fn();
396+
mockCreateFeishuReplyDispatcher.mockReturnValueOnce({
397+
dispatcher: {
398+
sendToolResult: vi.fn(),
399+
sendBlockReply: vi.fn(),
400+
sendFinalReply: vi.fn(),
401+
waitForIdle: vi.fn(),
402+
getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })),
403+
getFailedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 1 })),
404+
markComplete: vi.fn(),
405+
},
406+
replyOptions: {},
407+
markDispatchIdle: vi.fn(),
408+
ensureNoVisibleReplyFallback,
409+
});
410+
const cfg = createBroadcastConfig();
411+
const event = createBroadcastEvent({
412+
messageId: "msg-broadcast-final-failed",
413+
text: "hello @bot",
414+
botMentioned: true,
415+
});
416+
417+
await handleFeishuMessage({
418+
cfg,
419+
event,
420+
botOpenId: "bot-open-id",
421+
runtime: createRuntimeEnv(),
422+
});
423+
424+
expect(ensureNoVisibleReplyFallback).toHaveBeenCalledWith(
425+
"broadcast-dispatch-complete-no-visible-reply",
385426
);
386427
});
387428

extensions/feishu/src/bot.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): Plu
210210
onRecordError: turn.record?.onRecordError ?? (() => undefined),
211211
});
212212
return {
213+
dispatched: true,
213214
dispatchResult: await turn.runDispatch(),
214215
};
215216
}),
@@ -1088,6 +1089,48 @@ describe("handleFeishuMessage command authorization", () => {
10881089
expect(ensureNoVisibleReplyFallback).not.toHaveBeenCalled();
10891090
});
10901091

1092+
it("sends no-visible fallback when queued final delivery fails", async () => {
1093+
mockDispatchReplyFromConfig.mockResolvedValueOnce({
1094+
queuedFinal: true,
1095+
counts: { tool: 0, block: 0, final: 1 },
1096+
});
1097+
const ensureNoVisibleReplyFallback = vi.fn();
1098+
const dispatcher = createReplyDispatcher();
1099+
vi.mocked(dispatcher.getFailedCounts).mockReturnValue({ tool: 0, block: 0, final: 1 });
1100+
mockCreateFeishuReplyDispatcher.mockReturnValueOnce({
1101+
dispatcher,
1102+
replyOptions: {},
1103+
markDispatchIdle: vi.fn(),
1104+
ensureNoVisibleReplyFallback,
1105+
});
1106+
1107+
await dispatchMessage({
1108+
cfg: {
1109+
channels: {
1110+
feishu: {
1111+
dmPolicy: "open",
1112+
},
1113+
},
1114+
} as ClawdbotConfig,
1115+
event: {
1116+
sender: {
1117+
sender_id: {
1118+
open_id: "ou-sender",
1119+
},
1120+
},
1121+
message: {
1122+
message_id: "msg-final-delivery-failed",
1123+
chat_id: "oc-dm",
1124+
chat_type: "p2p",
1125+
message_type: "text",
1126+
content: JSON.stringify({ text: "hello" }),
1127+
},
1128+
},
1129+
});
1130+
1131+
expect(ensureNoVisibleReplyFallback).toHaveBeenCalledWith("dispatch-complete-no-visible-reply");
1132+
});
1133+
10911134
it("passes disabled config-write policy to dynamic agent creation", async () => {
10921135
mockShouldComputeCommandAuthorized.mockReturnValue(false);
10931136

extensions/feishu/src/bot.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,23 @@ type FeishuGroupSessionScope = "group" | "group_sender" | "group_topic" | "group
9090

9191
function shouldSendNoVisibleReplyFallback(dispatchResult: {
9292
counts: { final?: number };
93+
failedCounts?: { final?: number };
9394
noVisibleReplyFallbackEligible?: boolean;
9495
queuedFinal?: boolean;
9596
sendPolicyDenied?: boolean;
9697
sourceReplyDeliveryMode?: string;
9798
}): boolean {
98-
return (
99+
const finalCount = dispatchResult.counts.final ?? 0;
100+
const failedFinalCount = dispatchResult.failedCounts?.final ?? 0;
101+
const emptyEligibleDispatch =
99102
dispatchResult.noVisibleReplyFallbackEligible === true &&
100103
dispatchResult.queuedFinal !== true &&
101-
(dispatchResult.counts.final ?? 0) === 0 &&
104+
finalCount === 0;
105+
const queuedFinalFailed = dispatchResult.queuedFinal === true && failedFinalCount > 0;
106+
return (
102107
dispatchResult.sendPolicyDenied !== true &&
103-
dispatchResult.sourceReplyDeliveryMode !== "message_tool_only"
108+
dispatchResult.sourceReplyDeliveryMode !== "message_tool_only" &&
109+
(emptyEligibleDispatch || queuedFinalFailed)
104110
);
105111
}
106112

@@ -1567,9 +1573,12 @@ export async function handleFeishuMessage(params: {
15671573
});
15681574
if (
15691575
turnResult.dispatched &&
1570-
shouldSendNoVisibleReplyFallback(turnResult.dispatchResult)
1576+
shouldSendNoVisibleReplyFallback({
1577+
...turnResult.dispatchResult,
1578+
failedCounts: dispatcher.getFailedCounts(),
1579+
})
15711580
) {
1572-
await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-zero-final");
1581+
await ensureNoVisibleReplyFallback("broadcast-dispatch-complete-no-visible-reply");
15731582
}
15741583
} else {
15751584
// Observer agent: no-op dispatcher (session entry + inference, no Feishu reply).
@@ -1759,8 +1768,13 @@ export async function handleFeishuMessage(params: {
17591768
}
17601769
const { dispatchResult } = turnResult;
17611770
const { queuedFinal, counts } = dispatchResult;
1762-
if (shouldSendNoVisibleReplyFallback(dispatchResult)) {
1763-
await ensureNoVisibleReplyFallback("dispatch-complete-zero-final");
1771+
if (
1772+
shouldSendNoVisibleReplyFallback({
1773+
...dispatchResult,
1774+
failedCounts: dispatcher.getFailedCounts(),
1775+
})
1776+
) {
1777+
await ensureNoVisibleReplyFallback("dispatch-complete-no-visible-reply");
17641778
}
17651779

17661780
log(

0 commit comments

Comments
 (0)