diff --git a/extensions/whatsapp/src/auto-reply/monitor-state.test.ts b/extensions/whatsapp/src/auto-reply/monitor-state.test.ts new file mode 100644 index 00000000000..c0e0c218cf5 --- /dev/null +++ b/extensions/whatsapp/src/auto-reply/monitor-state.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { createWebChannelStatusController } from "./monitor-state.js"; + +describe("createWebChannelStatusController", () => { + it("sets lastTransportActivityAt on noteConnected", () => { + const patches: Record[] = []; + const controller = createWebChannelStatusController((s) => patches.push({ ...s })); + + controller.noteConnected(1000); + + const last = patches.at(-1)!; + expect(last.connected).toBe(true); + expect(last.lastTransportActivityAt).toBe(1000); + }); + + it("updates lastTransportActivityAt on noteInbound", () => { + const patches: Record[] = []; + const controller = createWebChannelStatusController((s) => patches.push({ ...s })); + + controller.noteConnected(1000); + controller.noteInbound(2000); + + const last = patches.at(-1)!; + expect(last.lastTransportActivityAt).toBe(2000); + }); + + it("does not set lastTransportActivityAt on noteWatchdogStale", () => { + const patches: Record[] = []; + const controller = createWebChannelStatusController((s) => patches.push({ ...s })); + + controller.noteConnected(1000); + controller.noteWatchdogStale(5000); + + const last = patches.at(-1)!; + // Watchdog staleness should not refresh transport activity — it means + // the check loop is running but the socket itself is idle/stale. + expect(last.lastTransportActivityAt).toBe(1000); + }); + + it("produces snapshots that enable stale-socket health detection", () => { + const patches: Record[] = []; + const controller = createWebChannelStatusController((s) => patches.push({ ...s })); + + controller.noteConnected(1000); + + const last = patches.at(-1)!; + // The gateway health policy checks `connected === true && lastTransportActivityAt != null` + // to decide whether to run stale-socket detection. Both must be present. + expect(last.connected).toBe(true); + expect(last.lastTransportActivityAt).not.toBeNull(); + expect(typeof last.lastTransportActivityAt).toBe("number"); + }); +}); diff --git a/extensions/whatsapp/src/auto-reply/monitor-state.ts b/extensions/whatsapp/src/auto-reply/monitor-state.ts index b03ed80f39b..77de22dd930 100644 --- a/extensions/whatsapp/src/auto-reply/monitor-state.ts +++ b/extensions/whatsapp/src/auto-reply/monitor-state.ts @@ -1,4 +1,7 @@ -import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime"; +import { + createConnectedChannelStatusPatch, + createTransportActivityStatusPatch, +} from "openclaw/plugin-sdk/gateway-runtime"; import type { WebChannelHealthState, WebChannelStatus } from "./types.js"; function cloneStatus(status: WebChannelStatus): WebChannelStatus { @@ -35,6 +38,7 @@ export function createWebChannelStatusController(statusSink?: (status: WebChanne snapshot: () => status, noteConnected(at = Date.now()) { Object.assign(status, createConnectedChannelStatusPatch(at)); + Object.assign(status, createTransportActivityStatusPatch(at)); status.lastError = null; status.healthState = "healthy"; emit(); @@ -43,6 +47,7 @@ export function createWebChannelStatusController(statusSink?: (status: WebChanne status.lastInboundAt = at; status.lastMessageAt = at; status.lastEventAt = at; + Object.assign(status, createTransportActivityStatusPatch(at)); if (status.connected) { status.healthState = "healthy"; } diff --git a/extensions/whatsapp/src/auto-reply/types.ts b/extensions/whatsapp/src/auto-reply/types.ts index 68c3ee545f6..5479b619d48 100644 --- a/extensions/whatsapp/src/auto-reply/types.ts +++ b/extensions/whatsapp/src/auto-reply/types.ts @@ -27,6 +27,7 @@ export type WebChannelStatus = { lastInboundAt?: number | null; lastMessageAt?: number | null; lastEventAt?: number | null; + lastTransportActivityAt?: number | null; lastError?: string | null; healthState?: WebChannelHealthState; };