diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index c16b2eb399..306d3cf472 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -3,7 +3,8 @@ import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry. import type { CliDeps } from "../cli/deps.js"; import { resolveAgentMaxConcurrent, resolveSubagentMaxConcurrent } from "../config/agent-limits.js"; import type { loadConfig } from "../config/config.js"; -import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js"; +import { startGmailWatcherWithLogs } from "../hooks/gmail-watcher-lifecycle.js"; +import { stopGmailWatcher } from "../hooks/gmail-watcher.js"; import { isTruthyEnvValue } from "../infra/env.js"; import type { HeartbeatRunner } from "../infra/heartbeat-runner.js"; import { resetDirectoryCache } from "../infra/outbound/target-resolver.js"; @@ -90,24 +91,12 @@ export function createGatewayReloadHandlers(params: { if (plan.restartGmailWatcher) { await stopGmailWatcher().catch(() => {}); - if (!isTruthyEnvValue(process.env.OPENCLAW_SKIP_GMAIL_WATCHER)) { - try { - const gmailResult = await startGmailWatcher(nextConfig); - if (gmailResult.started) { - params.logHooks.info("gmail watcher started"); - } else if ( - gmailResult.reason && - gmailResult.reason !== "hooks not enabled" && - gmailResult.reason !== "no gmail account configured" - ) { - params.logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`); - } - } catch (err) { - params.logHooks.error(`gmail watcher failed to start: ${String(err)}`); - } - } else { - params.logHooks.info("skipping gmail watcher restart (OPENCLAW_SKIP_GMAIL_WATCHER=1)"); - } + await startGmailWatcherWithLogs({ + cfg: nextConfig, + log: params.logHooks, + onSkipped: () => + params.logHooks.info("skipping gmail watcher restart (OPENCLAW_SKIP_GMAIL_WATCHER=1)"), + }); } if (plan.restartChannels.size > 0) { diff --git a/src/gateway/server-startup.ts b/src/gateway/server-startup.ts index 7ebda43ef9..15bf67f4c0 100644 --- a/src/gateway/server-startup.ts +++ b/src/gateway/server-startup.ts @@ -10,7 +10,7 @@ import { cleanStaleLockFiles } from "../agents/session-write-lock.js"; import type { CliDeps } from "../cli/deps.js"; import type { loadConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; -import { startGmailWatcher } from "../hooks/gmail-watcher.js"; +import { startGmailWatcherWithLogs } from "../hooks/gmail-watcher-lifecycle.js"; import { clearInternalHooks, createInternalHookEvent, @@ -68,22 +68,10 @@ export async function startGatewaySidecars(params: { } // Start Gmail watcher if configured (hooks.gmail.account). - if (!isTruthyEnvValue(process.env.OPENCLAW_SKIP_GMAIL_WATCHER)) { - try { - const gmailResult = await startGmailWatcher(params.cfg); - if (gmailResult.started) { - params.logHooks.info("gmail watcher started"); - } else if ( - gmailResult.reason && - gmailResult.reason !== "hooks not enabled" && - gmailResult.reason !== "no gmail account configured" - ) { - params.logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`); - } - } catch (err) { - params.logHooks.error(`gmail watcher failed to start: ${String(err)}`); - } - } + await startGmailWatcherWithLogs({ + cfg: params.cfg, + log: params.logHooks, + }); // Validate hooks.gmail.model if configured. if (params.cfg.hooks?.gmail?.model) { diff --git a/src/hooks/gmail-watcher-lifecycle.test.ts b/src/hooks/gmail-watcher-lifecycle.test.ts new file mode 100644 index 0000000000..8ded1e24f5 --- /dev/null +++ b/src/hooks/gmail-watcher-lifecycle.test.ts @@ -0,0 +1,94 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { startGmailWatcherMock } = vi.hoisted(() => ({ + startGmailWatcherMock: vi.fn(), +})); + +vi.mock("./gmail-watcher.js", () => ({ + startGmailWatcher: startGmailWatcherMock, +})); + +import { startGmailWatcherWithLogs } from "./gmail-watcher-lifecycle.js"; + +describe("startGmailWatcherWithLogs", () => { + const log = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + + beforeEach(() => { + startGmailWatcherMock.mockReset(); + log.info.mockReset(); + log.warn.mockReset(); + log.error.mockReset(); + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + }); + + afterEach(() => { + delete process.env.OPENCLAW_SKIP_GMAIL_WATCHER; + }); + + it("logs startup success", async () => { + startGmailWatcherMock.mockResolvedValue({ started: true, reason: undefined }); + + await startGmailWatcherWithLogs({ + cfg: {}, + log, + }); + + expect(log.info).toHaveBeenCalledWith("gmail watcher started"); + expect(log.warn).not.toHaveBeenCalled(); + expect(log.error).not.toHaveBeenCalled(); + }); + + it("logs actionable non-start reason", async () => { + startGmailWatcherMock.mockResolvedValue({ started: false, reason: "auth failed" }); + + await startGmailWatcherWithLogs({ + cfg: {}, + log, + }); + + expect(log.warn).toHaveBeenCalledWith("gmail watcher not started: auth failed"); + }); + + it("suppresses expected non-start reasons", async () => { + startGmailWatcherMock.mockResolvedValue({ + started: false, + reason: "hooks not enabled", + }); + + await startGmailWatcherWithLogs({ + cfg: {}, + log, + }); + + expect(log.warn).not.toHaveBeenCalled(); + }); + + it("supports skip callback when watcher is disabled", async () => { + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + const onSkipped = vi.fn(); + + await startGmailWatcherWithLogs({ + cfg: {}, + log, + onSkipped, + }); + + expect(startGmailWatcherMock).not.toHaveBeenCalled(); + expect(onSkipped).toHaveBeenCalledTimes(1); + }); + + it("logs startup errors", async () => { + startGmailWatcherMock.mockRejectedValue(new Error("boom")); + + await startGmailWatcherWithLogs({ + cfg: {}, + log, + }); + + expect(log.error).toHaveBeenCalledWith("gmail watcher failed to start: Error: boom"); + }); +}); diff --git a/src/hooks/gmail-watcher-lifecycle.ts b/src/hooks/gmail-watcher-lifecycle.ts new file mode 100644 index 0000000000..975206b34c --- /dev/null +++ b/src/hooks/gmail-watcher-lifecycle.ts @@ -0,0 +1,37 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { isTruthyEnvValue } from "../infra/env.js"; +import { startGmailWatcher } from "./gmail-watcher.js"; + +export type GMailWatcherLog = { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; +}; + +export async function startGmailWatcherWithLogs(params: { + cfg: OpenClawConfig; + log: GMailWatcherLog; + onSkipped?: () => void; +}) { + if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_GMAIL_WATCHER)) { + params.onSkipped?.(); + return; + } + + try { + const gmailResult = await startGmailWatcher(params.cfg); + if (gmailResult.started) { + params.log.info("gmail watcher started"); + return; + } + if ( + gmailResult.reason && + gmailResult.reason !== "hooks not enabled" && + gmailResult.reason !== "no gmail account configured" + ) { + params.log.warn(`gmail watcher not started: ${gmailResult.reason}`); + } + } catch (err) { + params.log.error(`gmail watcher failed to start: ${String(err)}`); + } +}