fix(telegram): bound error cooldown expiry

This commit is contained in:
Peter Steinberger
2026-05-30 13:59:06 -04:00
parent 8654353be8
commit 1f6c1eacf0
2 changed files with 85 additions and 4 deletions

View File

@@ -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",

View File

@@ -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<string, number>, 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<string, number>();
nextScopeStore.set(messageKey, now + cooldownMs);
nextScopeStore.set(messageKey, nextExpiresAt);
errorCooldownStore.set(scopeKey, nextScopeStore);
return false;
}