fix(plugins): tighten interactive callback handling

This commit is contained in:
Vincent Koc
2026-03-13 14:22:55 -07:00
parent 50df0bb00e
commit 0f4775148c
8 changed files with 132 additions and 3 deletions

View File

@@ -146,4 +146,56 @@ describe("plugin interactive handlers", () => {
}),
);
});
it("does not consume dedupe keys when a handler throws", async () => {
const handler = vi
.fn(async () => ({ handled: true }))
.mockRejectedValueOnce(new Error("boom"))
.mockResolvedValueOnce({ handled: true });
expect(
registerPluginInteractiveHandler("codex-plugin", {
channel: "telegram",
namespace: "codex",
handler,
}),
).toEqual({ ok: true });
const baseParams = {
channel: "telegram" as const,
data: "codex:resume:thread-1",
callbackId: "cb-throw",
ctx: {
accountId: "default",
callbackId: "cb-throw",
conversationId: "-10099:topic:77",
parentConversationId: "-10099",
senderId: "user-1",
senderUsername: "ada",
threadId: 77,
isGroup: true,
isForum: true,
auth: { isAuthorizedSender: true },
callbackMessage: {
messageId: 55,
chatId: "-10099",
messageText: "Pick a thread",
},
},
respond: {
reply: vi.fn(async () => {}),
editMessage: vi.fn(async () => {}),
editButtons: vi.fn(async () => {}),
clearButtons: vi.fn(async () => {}),
deleteMessage: vi.fn(async () => {}),
},
};
await expect(dispatchPluginInteractiveHandler(baseParams)).rejects.toThrow("boom");
await expect(dispatchPluginInteractiveHandler(baseParams)).resolves.toEqual({
matched: true,
handled: true,
duplicate: false,
});
expect(handler).toHaveBeenCalledTimes(2);
});
});

View File

@@ -187,7 +187,7 @@ export async function dispatchPluginInteractiveHandler(params: {
const dedupeKey =
params.channel === "telegram" ? params.callbackId?.trim() : params.interactionId?.trim();
if (dedupeKey && callbackDedupe.check(dedupeKey)) {
if (dedupeKey && callbackDedupe.peek(dedupeKey)) {
return { matched: true, handled: true, duplicate: true };
}
@@ -253,6 +253,9 @@ export async function dispatchPluginInteractiveHandler(params: {
});
}
const resolved = await result;
if (dedupeKey) {
callbackDedupe.check(dedupeKey);
}
return {
matched: true,

View File

@@ -61,4 +61,23 @@ describe("createTelegramTypingLease", () => {
lease.stop();
});
it("falls back to the default interval for non-finite values", async () => {
vi.useFakeTimers();
const pulse = vi.fn(async () => undefined);
const lease = await createTelegramTypingLease({
to: "telegram:123",
intervalMs: Number.NaN,
pulse,
});
expect(pulse).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(3_999);
expect(pulse).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
expect(pulse).toHaveBeenCalledTimes(2);
lease.stop();
});
});

View File

@@ -19,7 +19,10 @@ export async function createTelegramTypingLease(params: CreateTelegramTypingLeas
refresh: () => Promise<void>;
stop: () => void;
}> {
const intervalMs = Math.max(1000, Math.floor(params.intervalMs ?? 4_000));
const intervalMs =
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
? Math.max(1_000, Math.floor(params.intervalMs))
: 4_000;
let stopped = false;
const refresh = async () => {