diff --git a/extensions/feishu/src/probe.test.ts b/extensions/feishu/src/probe.test.ts index adcb8406e603..787048aeea73 100644 --- a/extensions/feishu/src/probe.test.ts +++ b/extensions/feishu/src/probe.test.ts @@ -187,6 +187,32 @@ describe("probeFeishu", () => { expect(requestFn).toHaveBeenCalledTimes(1); }); + it("does not cache probe results when the expiry would exceed a valid Date", async () => { + await withFakeTimers(async () => { + vi.setSystemTime(new Date(8_640_000_000_000_000)); + const requestFn = setupSuccessClient(); + + const { first, second } = await readSequentialDefaultProbePair(); + + expect(first).toEqual(second); + expect(requestFn).toHaveBeenCalledTimes(2); + }); + }); + + it("evicts cached probe results when the current clock is invalid", async () => { + const requestFn = setupSuccessClient(); + + await probeFeishu(DEFAULT_CREDS); + const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); + try { + await probeFeishu(DEFAULT_CREDS); + } finally { + dateNow.mockRestore(); + } + + expect(requestFn).toHaveBeenCalledTimes(2); + }); + it("makes a fresh API call after cache expires", async () => { await withFakeTimers(async () => { const requestFn = setupSuccessClient(); diff --git a/extensions/feishu/src/probe.ts b/extensions/feishu/src/probe.ts index 766677793ff8..7a4ec424015f 100644 --- a/extensions/feishu/src/probe.ts +++ b/extensions/feishu/src/probe.ts @@ -1,4 +1,8 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { raceWithTimeoutAndAbort } from "./async.js"; import { createFeishuClient, type FeishuClientCredentials } from "./client.js"; import type { FeishuProbeResult } from "./types.js"; @@ -38,7 +42,12 @@ function setCachedProbeResult( result: FeishuProbeResult, ttlMs: number, ): FeishuProbeResult { - probeCache.set(cacheKey, { result, expiresAt: Date.now() + ttlMs }); + const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs); + if (expiresAt === undefined) { + probeCache.delete(cacheKey); + return result; + } + probeCache.set(cacheKey, { result, expiresAt }); if (probeCache.size > MAX_PROBE_CACHE_SIZE) { const oldest = probeCache.keys().next().value; if (oldest !== undefined) { @@ -74,8 +83,13 @@ export async function probeFeishu( // pollute each other's cache entry. const cacheKey = creds.accountId ?? `${creds.appId}:${creds.appSecret.slice(0, 8)}`; const cached = probeCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) { - return cached.result; + if (cached) { + const now = asDateTimestampMs(Date.now()); + const expiresAt = asDateTimestampMs(cached.expiresAt); + if (now !== undefined && expiresAt !== undefined && expiresAt > now) { + return cached.result; + } + probeCache.delete(cacheKey); } try {