fix(feishu): make card action retries explicit

This commit is contained in:
Vincent Koc
2026-04-13 16:08:24 +01:00
parent f881f086bb
commit 67593a8108
2 changed files with 33 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js";
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
import {
FeishuRetryableCardActionError,
handleFeishuCardAction,
resetProcessedFeishuCardActionTokensForTests,
type FeishuCardActionEvent,
@@ -88,6 +89,9 @@ describe("Feishu Card Action Handler", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(handleFeishuMessage)
.mockReset()
.mockResolvedValue(undefined as never);
resetProcessedFeishuCardActionTokensForTests();
});
@@ -363,7 +367,7 @@ describe("Feishu Card Action Handler", () => {
expect(handleFeishuMessage).toHaveBeenCalledTimes(1);
});
it("releases a claimed token when dispatch fails so retries can succeed", async () => {
it("keeps a claimed token completed after a non-retryable dispatch failure", async () => {
const event = createStructuredQuickActionEvent({
token: "tok11",
action: "feishu.quick_actions.help",
@@ -376,6 +380,22 @@ describe("Feishu Card Action Handler", () => {
await expect(handleFeishuCardAction({ cfg, event, runtime })).rejects.toThrow("transient");
await handleFeishuCardAction({ cfg, event, runtime });
expect(handleFeishuMessage).toHaveBeenCalledTimes(1);
});
it("releases a claimed token for explicit retryable dispatch failures", async () => {
const event = createStructuredQuickActionEvent({
token: "tok11-retryable",
action: "feishu.quick_actions.help",
command: "/help",
});
vi.mocked(handleFeishuMessage)
.mockRejectedValueOnce(new FeishuRetryableCardActionError("retry me"))
.mockResolvedValueOnce(undefined as never);
await expect(handleFeishuCardAction({ cfg, event, runtime })).rejects.toThrow("retry me");
await handleFeishuCardAction({ cfg, event, runtime });
expect(handleFeishuMessage).toHaveBeenCalledTimes(2);
});

View File

@@ -35,6 +35,13 @@ const processedCardActionTokens = new Map<
{ status: "inflight" | "completed"; expiresAt: number }
>();
export class FeishuRetryableCardActionError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "FeishuRetryableCardActionError";
}
}
export function resetProcessedFeishuCardActionTokensForTests(): void {
processedCardActionTokens.clear();
}
@@ -304,7 +311,11 @@ export async function handleFeishuCardAction(params: {
});
completeFeishuCardActionToken({ token: event.token, accountId: account.accountId });
} catch (err) {
releaseFeishuCardActionToken({ token: event.token, accountId: account.accountId });
if (err instanceof FeishuRetryableCardActionError) {
releaseFeishuCardActionToken({ token: event.token, accountId: account.accountId });
} else {
completeFeishuCardActionToken({ token: event.token, accountId: account.accountId });
}
throw err;
}
}