diff --git a/extensions/signal/src/client-adapter.test.ts b/extensions/signal/src/client-adapter.test.ts index 4701bdf8c77..27eec13003f 100644 --- a/extensions/signal/src/client-adapter.test.ts +++ b/extensions/signal/src/client-adapter.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { signalRpcRequest as signalRpcRequestImpl, detectSignalApiMode, @@ -37,6 +37,11 @@ beforeEach(() => { ); }); +afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); +}); + function setApiMode(mode: SignalApiMode) { currentApiMode = mode; } @@ -378,6 +383,44 @@ describe("signalCheck", () => { error: "Signal API not reachable at http://localhost:8080", }); }); + + it("drops cached auto mode when the current clock is not a valid date timestamp", async () => { + setApiMode("auto"); + vi.spyOn(Date, "now").mockReturnValueOnce(1_700_000_000_000).mockReturnValueOnce(Number.NaN); + mockNativeCheck.mockResolvedValue({ ok: true, status: 200 }); + mockContainerCheck.mockResolvedValue({ ok: false, status: 404 }); + + await expect(signalCheck("http://auto-invalid-clock.local:8080")).resolves.toEqual({ + ok: true, + status: 200, + }); + await expect(signalCheck("http://auto-invalid-clock.local:8080")).resolves.toEqual({ + ok: true, + status: 200, + }); + + expect(mockNativeCheck).toHaveBeenCalledTimes(4); + expect(mockContainerCheck).toHaveBeenCalledTimes(2); + }); + + it("does not cache auto mode when the expiry timestamp would exceed the valid date range", async () => { + setApiMode("auto"); + vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000); + mockNativeCheck.mockResolvedValue({ ok: true, status: 200 }); + mockContainerCheck.mockResolvedValue({ ok: false, status: 404 }); + + await expect(signalCheck("http://auto-overflow-clock.local:8080")).resolves.toEqual({ + ok: true, + status: 200, + }); + await expect(signalCheck("http://auto-overflow-clock.local:8080")).resolves.toEqual({ + ok: true, + status: 200, + }); + + expect(mockNativeCheck).toHaveBeenCalledTimes(4); + expect(mockContainerCheck).toHaveBeenCalledTimes(2); + }); }); describe("streamSignalEvents", () => { diff --git a/extensions/signal/src/client-adapter.ts b/extensions/signal/src/client-adapter.ts index 94778bdb944..13ad59b5e78 100644 --- a/extensions/signal/src/client-adapter.ts +++ b/extensions/signal/src/client-adapter.ts @@ -6,6 +6,10 @@ * only need to change their import path. */ +import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { containerCheck, containerRpcRequest, @@ -74,24 +78,33 @@ async function resolveAutoApiMode( timeoutMs = DEFAULT_TIMEOUT_MS, options: { account?: string; requireContainerReceive?: boolean } = {}, ): Promise<"native" | "container"> { + const rawNow = Date.now(); + const now = asDateTimestampMs(rawNow); const cached = detectedModeCache.get(baseUrl); - if (cached && cached.expiresAt > Date.now()) { - if ( - cached.mode !== "container" || - !options.requireContainerReceive || - (Boolean(options.account?.trim()) && cached.receiveAccount === options.account?.trim()) - ) { - return cached.mode; + if (cached) { + if (now !== undefined && cached.expiresAt > now) { + if ( + cached.mode !== "container" || + !options.requireContainerReceive || + (Boolean(options.account?.trim()) && cached.receiveAccount === options.account?.trim()) + ) { + return cached.mode; + } + } else { + detectedModeCache.delete(baseUrl); } } const detected = await detectSignalApiMode(baseUrl, timeoutMs, options); - detectedModeCache.set(baseUrl, { - mode: detected, - expiresAt: Date.now() + MODE_CACHE_TTL_MS, - ...(detected === "container" && options.requireContainerReceive && options.account - ? { receiveAccount: options.account } - : {}), - }); + const expiresAt = resolveExpiresAtMsFromDurationMs(MODE_CACHE_TTL_MS, { nowMs: rawNow }); + if (expiresAt !== undefined) { + detectedModeCache.set(baseUrl, { + mode: detected, + expiresAt, + ...(detected === "container" && options.requireContainerReceive && options.account + ? { receiveAccount: options.account } + : {}), + }); + } return detected; }