From 53575f20139139d9b27e15e82892b55b99cb4a02 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 22:48:54 +0100 Subject: [PATCH] fix: add googlechat lifecycle regression test (#27384) (thanks @junsuwhy) --- CHANGELOG.md | 1 + .../googlechat/src/channel.startup.test.ts | 102 ++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 extensions/googlechat/src/channel.startup.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ce05d981340..85fa1ca62a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Delivery queue/recovery backoff: prevent retry starvation by persisting `lastAttemptAt` on failed sends and deferring recovery retries until each entry's `lastAttemptAt + backoff` window is eligible, while continuing to recover ready entries behind deferred ones. Landed from contributor PR #27710 by @Jimmy-xuzimo. Thanks @Jimmy-xuzimo. +- Google Chat/Lifecycle: keep Google Chat `startAccount` pending until abort in webhook mode so startup is no longer interpreted as immediate exit, preventing auto-restart loops and webhook-target churn. (#27384) thanks @junsuwhy. - Microsoft Teams/File uploads: acknowledge `fileConsent/invoke` immediately (`invokeResponse` before upload + file card send) so Teams no longer shows false "Something went wrong" timeout banners while upload completion continues asynchronously; includes updated async regression coverage. Landed from contributor PR #27641 by @scz2011. - Queue/Drain/Cron reliability: harden lane draining with guaranteed `draining` flag reset on synchronous pump failures, reject new queue enqueues during gateway restart drain windows (instead of silently killing accepted tasks), add `/stop` queued-backlog cutoff metadata with stale-message skipping (while avoiding cross-session native-stop cutoff bleed), and raise isolated cron `agentTurn` outer safety timeout to avoid false 10-minute timeout races against longer agent session timeouts. (#27407, #27332, #27427) - Typing/Main reply pipeline: always mark dispatch idle in `agent-runner` finalization so typing cleanup runs even when dispatcher `onIdle` does not fire, preventing stuck typing indicators after run completion. (#27250) Thanks @Sid-Qin. diff --git a/extensions/googlechat/src/channel.startup.test.ts b/extensions/googlechat/src/channel.startup.test.ts new file mode 100644 index 00000000000..8823775cfd6 --- /dev/null +++ b/extensions/googlechat/src/channel.startup.test.ts @@ -0,0 +1,102 @@ +import type { + ChannelAccountSnapshot, + ChannelGatewayContext, + OpenClawConfig, +} from "openclaw/plugin-sdk"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRuntimeEnv } from "../../test-utils/runtime-env.js"; +import type { ResolvedGoogleChatAccount } from "./accounts.js"; + +const hoisted = vi.hoisted(() => ({ + startGoogleChatMonitor: vi.fn(), +})); + +vi.mock("./monitor.js", async () => { + const actual = await vi.importActual("./monitor.js"); + return { + ...actual, + startGoogleChatMonitor: hoisted.startGoogleChatMonitor, + }; +}); + +import { googlechatPlugin } from "./channel.js"; + +function createStartAccountCtx(params: { + account: ResolvedGoogleChatAccount; + abortSignal: AbortSignal; + statusPatchSink?: (next: ChannelAccountSnapshot) => void; +}): ChannelGatewayContext { + const snapshot: ChannelAccountSnapshot = { + accountId: params.account.accountId, + configured: true, + enabled: true, + running: false, + }; + return { + accountId: params.account.accountId, + account: params.account, + cfg: {} as OpenClawConfig, + runtime: createRuntimeEnv(), + abortSignal: params.abortSignal, + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, + getStatus: () => snapshot, + setStatus: (next) => { + Object.assign(snapshot, next); + params.statusPatchSink?.(snapshot); + }, + }; +} + +describe("googlechatPlugin gateway.startAccount", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("keeps startAccount pending until abort, then unregisters", async () => { + const unregister = vi.fn(); + hoisted.startGoogleChatMonitor.mockResolvedValue(unregister); + + const account: ResolvedGoogleChatAccount = { + accountId: "default", + enabled: true, + credentialSource: "inline", + credentials: {}, + config: { + webhookPath: "/googlechat", + webhookUrl: "https://example.com/googlechat", + audienceType: "app-url", + audience: "https://example.com/googlechat", + }, + }; + + const patches: ChannelAccountSnapshot[] = []; + const abort = new AbortController(); + const task = googlechatPlugin.gateway!.startAccount!( + createStartAccountCtx({ + account, + abortSignal: abort.signal, + statusPatchSink: (next) => patches.push({ ...next }), + }), + ); + + await new Promise((resolve) => setTimeout(resolve, 20)); + + let settled = false; + void task.then(() => { + settled = true; + }); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(settled).toBe(false); + + expect(hoisted.startGoogleChatMonitor).toHaveBeenCalledOnce(); + expect(unregister).not.toHaveBeenCalled(); + + abort.abort(); + await task; + + expect(unregister).toHaveBeenCalledOnce(); + expect(patches.some((entry) => entry.running === true)).toBe(true); + expect(patches.some((entry) => entry.running === false)).toBe(true); + }); +});