diff --git a/extensions/telegram/src/error-policy.test.ts b/extensions/telegram/src/error-policy.test.ts index 741021d0d35..69c58e2f919 100644 --- a/extensions/telegram/src/error-policy.test.ts +++ b/extensions/telegram/src/error-policy.test.ts @@ -1,3 +1,4 @@ +import { MAX_DATE_TIMESTAMP_MS } from "openclaw/plugin-sdk/number-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { buildTelegramErrorScopeKey, @@ -120,6 +121,72 @@ describe("telegram error policy", () => { ).toBe(false); }); + it("does not suppress or keep cooldowns when the process clock is invalid", () => { + const scopeKey = buildTelegramErrorScopeKey({ + accountId: "work", + chatId: 42, + }); + + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(false); + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(true); + + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); + try { + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(false); + } finally { + nowSpy.mockRestore(); + } + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(false); + }); + + it("does not store cooldowns whose expiry would exceed the Date range", () => { + const scopeKey = buildTelegramErrorScopeKey({ + accountId: "work", + chatId: 42, + }); + vi.setSystemTime(MAX_DATE_TIMESTAMP_MS); + + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(false); + vi.setSystemTime(new Date("2026-01-01T00:00:00Z")); + expect( + shouldSuppressTelegramError({ + scopeKey, + cooldownMs: 1000, + errorMessage: "429", + }), + ).toBe(false); + }); + it("does not leak suppression across accounts or threads", () => { const workMain = buildTelegramErrorScopeKey({ accountId: "work", diff --git a/extensions/telegram/src/error-policy.ts b/extensions/telegram/src/error-policy.ts index 90f18b2a714..b7a01547945 100644 --- a/extensions/telegram/src/error-policy.ts +++ b/extensions/telegram/src/error-policy.ts @@ -4,6 +4,11 @@ import type { TelegramGroupConfig, TelegramTopicConfig, } from "openclaw/plugin-sdk/config-contracts"; +import { + asDateTimestampMs, + isFutureDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; type TelegramErrorPolicy = "always" | "once" | "silent"; @@ -18,7 +23,7 @@ const DEFAULT_ERROR_COOLDOWN_MS = 14400000; function pruneExpiredCooldowns(messageStore: Map, now: number) { for (const [message, expiresAt] of messageStore) { - if (expiresAt <= now) { + if (!isFutureDateTimestampMs(expiresAt, { nowMs: now })) { messageStore.delete(message); } } @@ -67,9 +72,13 @@ export function shouldSuppressTelegramError(params: { errorMessage?: string; }): boolean { const { scopeKey, cooldownMs, errorMessage } = params; - const now = Date.now(); + const now = asDateTimestampMs(Date.now()); const messageKey = errorMessage ?? ""; const scopeStore = errorCooldownStore.get(scopeKey); + if (now === undefined) { + errorCooldownStore.delete(scopeKey); + return false; + } if (scopeStore) { pruneExpiredCooldowns(scopeStore, now); @@ -88,12 +97,17 @@ export function shouldSuppressTelegramError(params: { } const expiresAt = scopeStore?.get(messageKey); - if (typeof expiresAt === "number" && expiresAt > now) { + if (isFutureDateTimestampMs(expiresAt, { nowMs: now })) { return true; } + const nextExpiresAt = resolveExpiresAtMsFromDurationMs(cooldownMs, { nowMs: now }); + if (nextExpiresAt === undefined) { + scopeStore?.delete(messageKey); + return false; + } const nextScopeStore = scopeStore ?? new Map(); - nextScopeStore.set(messageKey, now + cooldownMs); + nextScopeStore.set(messageKey, nextExpiresAt); errorCooldownStore.set(scopeKey, nextScopeStore); return false; }