fix(feishu): preserve accepted reply chunks without receipts (#128213)

This commit is contained in:
Peter Steinberger
2026-08-23 06:01:42 -07:00
committed by GitHub
parent 16a800ebf1
commit 2aa4b694e1
2 changed files with 62 additions and 7 deletions
+52 -1
View File
@@ -1638,7 +1638,14 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
.deliver({ text }, { kind: "final" })
.catch((caught: unknown) => caught);
expect(isChannelPartialDeliveryError(error)).toBe(true);
expect(error).toMatchObject({
code: "CHANNEL_PARTIAL_DELIVERY",
deliveryResult: {
content: text,
messageIds: [],
visibleReplySent: true,
},
});
expect(provider).toHaveBeenCalledOnce();
await Promise.resolve(options.onError?.(error, { kind: "final" }));
expect(result.getVisibleReplyState().visibleReplySent).toBe(true);
@@ -1650,6 +1657,50 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
},
);
it.each([
{ kind: "text", provider: sendMessageFeishuMock, acceptedBeforeReceiptLoss: 0 },
{ kind: "text", provider: sendMessageFeishuMock, acceptedBeforeReceiptLoss: 1 },
{ kind: "card", provider: sendStructuredCardFeishuMock, acceptedBeforeReceiptLoss: 0 },
{ kind: "card", provider: sendStructuredCardFeishuMock, acceptedBeforeReceiptLoss: 1 },
])(
"retains accepted $kind chunk content after receipt loss with $acceptedBeforeReceiptLoss prior receipts",
async ({ kind, provider, acceptedBeforeReceiptLoss }) => {
useNonStreamingAutoAccount();
const runtime = getFeishuRuntimeMock();
runtime.channel.text.resolveTextChunkLimit.mockReturnValue(6);
runtime.channel.text.chunkMarkdownTextWithMode.mockReturnValue(["first", "second", "third"]);
if (acceptedBeforeReceiptLoss > 0) {
provider.mockResolvedValueOnce({ messageId: "om-first" });
}
provider.mockRejectedValueOnce(
createChannelPartialDeliveryError(
new Error("Feishu reply failed: no message_id returned"),
{
messageIds: [],
visibleReplySent: true,
},
),
);
const { options } = createDispatcherHarness();
const text = kind === "card" ? "| first | second |\n| - | - |" : "firstsecondthird";
const error = await options
.deliver({ text }, { kind: "final" })
.catch((caught: unknown) => caught);
expect(error).toMatchObject({
code: "CHANNEL_PARTIAL_DELIVERY",
deliveryResult: {
content: acceptedBeforeReceiptLoss > 0 ? "firstsecond" : "first",
messageIds: acceptedBeforeReceiptLoss > 0 ? ["om-first"] : [],
visibleReplySent: true,
},
});
expect(provider).toHaveBeenCalledTimes(acceptedBeforeReceiptLoss + 1);
},
);
it("retains the finalized streaming card when companion media never dispatches", async () => {
const marker = Object.assign(
new Error("media load failed", { cause: new Error("blocked local load") }),
+10 -6
View File
@@ -763,18 +763,22 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
acceptedChunks.push(chunk);
markVisibleReplySent();
} catch (error: unknown) {
if (isChannelPartialDeliveryError(error)) {
const acceptedChunk = isChannelPartialDeliveryError(error)
? error.deliveryResult
: undefined;
if (acceptedChunk) {
acceptedChunks.push(acceptedChunk.content ?? chunk);
markVisibleReplySent();
}
throw createFeishuPartialReplyDeliveryError(
error,
createFeishuReplyDeliveryResult({
throw createFeishuPartialReplyDeliveryError(error, {
...acceptedChunk,
...createFeishuReplyDeliveryResult({
results,
visibleReplySent: results.length > 0,
visibleReplySent: results.length > 0 || acceptedChunk !== undefined,
content: acceptedChunks.join(""),
kind: paramsLocal.useCard ? "card" : "text",
}),
);
});
}
}
if (paramsLocal.infoKind === "final") {