

























@@ -301,4 +301,94 @@ describe("createTelegramSendChatActionHandler", () => {
301301await handler.sendChatAction(444, "typing");
302302expect(fn).toHaveBeenCalledTimes(3);
303303});
304+305+it("treats a structured 429 with retry_after=401 as transient, not as a 401 suspension", async () => {
306+// grammY renders this as: "Call to 'sendChatAction' failed! (429: Too Many Requests: retry after 401)"
307+// The substring "401" in the message must NOT trigger the 401 suspension path.
308+const make429WithRetryAfter401 = () =>
309+Object.assign(
310+new Error("Call to 'sendChatAction' failed! (429: Too Many Requests: retry after 401)"),
311+{ error_code: 429, parameters: { retry_after: 401 } },
312+);
313+314+let now = 0;
315+const fn = vi.fn().mockRejectedValue(make429WithRetryAfter401());
316+const logger = vi.fn();
317+const handler = createTelegramSendChatActionHandler({
318+sendChatActionFn: fn,
319+ logger,
320+maxConsecutive401: 3,
321+now: () => now,
322+});
323+324+// All calls should fail but as transient errors, NOT 401 errors
325+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("429");
326+// Advance past the transient cooldown (retry_after=401 → 401000ms)
327+now += 402_000;
328+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("429");
329+now += 402_000;
330+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("429");
331+332+// Handler must NOT be suspended — this is a transient 429, not a 401
333+expect(handler.isSuspended()).toBe(false);
334+335+// Must NOT have logged the CRITICAL token-deletion alarm
336+const criticalLogs = logger.mock.calls.filter(([msg]) => String(msg).includes("CRITICAL"));
337+expect(criticalLogs).toEqual([]);
338+});
339+340+it("detects a structured Telegram 401 error by error_code, not by message substring", async () => {
341+const makeStructured401 = () => Object.assign(new Error("Unauthorized"), { error_code: 401 });
342+343+const fn = vi.fn().mockRejectedValue(makeStructured401());
344+const logger = vi.fn();
345+const handler = createTelegramSendChatActionHandler({
346+sendChatActionFn: fn,
347+ logger,
348+maxConsecutive401: 2,
349+});
350+351+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("Unauthorized");
352+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("Unauthorized");
353+354+expect(handler.isSuspended()).toBe(true);
355+expect(logger.mock.calls.at(-1)).toEqual([
356+"CRITICAL: sendChatAction suspended after 2 consecutive 401 errors. Bot token is likely invalid. Telegram may DELETE the bot if requests continue. Replace the token and restart: openclaw channels restart telegram",
357+]);
358+});
359+360+it("does not misclassify a plain error whose message contains '401' as a 401 error", async () => {
361+// A plain Error (no error_code) with "401" in its message should NOT
362+// trigger the 401 path, since bare substring matching was the root cause
363+// of #94787. Only "unauthorized" is the fallback for non-Telegram errors.
364+const fn = vi.fn().mockRejectedValue(new Error("401 Too Many Requests: retry after"));
365+const logger = vi.fn();
366+const handler = createTelegramSendChatActionHandler({
367+sendChatActionFn: fn,
368+ logger,
369+maxConsecutive401: 3,
370+});
371+372+// This is neither a recognized 401 nor a recognized transient → falls to
373+// the "else" branch (clears transient cooldown, re-throws).
374+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("401");
375+expect(handler.isSuspended()).toBe(false);
376+});
377+378+it("recognizes non-Telegram 401 via 'unauthorized' in message as a 401", async () => {
379+// A plain Error without error_code but with "unauthorized" should still
380+// be classified as 401 (this is the intentional fallback path).
381+const fn = vi.fn().mockRejectedValue(new Error("Unauthorized access"));
382+const logger = vi.fn();
383+const handler = createTelegramSendChatActionHandler({
384+sendChatActionFn: fn,
385+ logger,
386+maxConsecutive401: 2,
387+});
388+389+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("Unauthorized");
390+await expect(handler.sendChatAction(123, "typing")).rejects.toThrow("Unauthorized");
391+392+expect(handler.isSuspended()).toBe(true);
393+});
304394});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。