import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; import type { OpenClawConfig } from "../runtime-api.js"; import type { ResolvedZaloAccount } from "./accounts.js"; const getWebhookInfoMock = vi.fn(async () => ({ ok: true, result: { url: "" } })); const deleteWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } })); const getUpdatesMock = vi.fn(() => new Promise(() => {})); const setWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } })); vi.mock("./api.js", async () => { const actual = await vi.importActual("./api.js"); return { ...actual, deleteWebhook: deleteWebhookMock, getWebhookInfo: getWebhookInfoMock, getUpdates: getUpdatesMock, setWebhook: setWebhookMock, }; }); vi.mock("./runtime.js", () => ({ getZaloRuntime: () => ({ logging: { shouldLogVerbose: () => false, }, }), })); const TEST_ACCOUNT = { accountId: "default", config: {}, } as unknown as ResolvedZaloAccount; const TEST_CONFIG = {} as OpenClawConfig; async function startLifecycleMonitor( options: { useWebhook?: boolean; webhookSecret?: string; webhookUrl?: string; } = {}, ) { const { monitorZaloProvider } = await import("./monitor.js"); const abort = new AbortController(); const runtime = createRuntimeEnv(); const run = monitorZaloProvider({ token: "test-token", account: TEST_ACCOUNT, config: TEST_CONFIG, runtime, abortSignal: abort.signal, ...options, }); return { abort, runtime, run }; } describe("monitorZaloProvider lifecycle", () => { afterEach(() => { vi.clearAllMocks(); setActivePluginRegistry(createEmptyPluginRegistry()); }); it("stays alive in polling mode until abort", async () => { let settled = false; const { abort, runtime, run } = await startLifecycleMonitor(); const monitoredRun = run.then(() => { settled = true; }); await vi.waitFor(() => expect(getUpdatesMock).toHaveBeenCalledTimes(1)); expect(getWebhookInfoMock).toHaveBeenCalledTimes(1); expect(deleteWebhookMock).not.toHaveBeenCalled(); expect(getUpdatesMock).toHaveBeenCalledTimes(1); expect(settled).toBe(false); abort.abort(); await monitoredRun; expect(settled).toBe(true); expect(runtime.log).toHaveBeenCalledWith( expect.stringContaining("Zalo provider stopped mode=polling"), ); }); it("deletes an existing webhook before polling", async () => { getWebhookInfoMock.mockResolvedValueOnce({ ok: true, result: { url: "https://example.com/hooks/zalo" }, }); const { abort, runtime, run } = await startLifecycleMonitor(); await vi.waitFor(() => expect(getUpdatesMock).toHaveBeenCalledTimes(1)); expect(getWebhookInfoMock).toHaveBeenCalledTimes(1); expect(deleteWebhookMock).toHaveBeenCalledTimes(1); expect(runtime.log).toHaveBeenCalledWith( expect.stringContaining("Zalo polling mode ready (webhook disabled)"), ); abort.abort(); await run; }); it("continues polling when webhook inspection returns 404", async () => { const { ZaloApiError } = await import("./api.js"); getWebhookInfoMock.mockRejectedValueOnce(new ZaloApiError("Not Found", 404, "Not Found")); const { abort, runtime, run } = await startLifecycleMonitor(); await vi.waitFor(() => expect(getUpdatesMock).toHaveBeenCalledTimes(1)); expect(getWebhookInfoMock).toHaveBeenCalledTimes(1); expect(deleteWebhookMock).not.toHaveBeenCalled(); expect(runtime.log).toHaveBeenCalledWith( expect.stringContaining("webhook inspection unavailable; continuing without webhook cleanup"), ); expect(runtime.error).not.toHaveBeenCalled(); abort.abort(); await run; }); it("waits for webhook deletion before finishing webhook shutdown", async () => { const registry = createEmptyPluginRegistry(); setActivePluginRegistry(registry); let resolveDeleteWebhook: (() => void) | undefined; deleteWebhookMock.mockImplementationOnce( () => new Promise((resolve) => { resolveDeleteWebhook = () => resolve({ ok: true, result: { url: "" } }); }), ); let settled = false; const { abort, runtime, run } = await startLifecycleMonitor({ useWebhook: true, webhookUrl: "https://example.com/hooks/zalo", webhookSecret: "supersecret", // pragma: allowlist secret }); const monitoredRun = run.then(() => { settled = true; }); await vi.waitFor(() => expect(setWebhookMock).toHaveBeenCalledTimes(1)); expect(registry.httpRoutes).toHaveLength(1); abort.abort(); await vi.waitFor(() => expect(deleteWebhookMock).toHaveBeenCalledTimes(1)); expect(deleteWebhookMock).toHaveBeenCalledWith("test-token", undefined, 5000); expect(settled).toBe(false); expect(registry.httpRoutes).toHaveLength(1); resolveDeleteWebhook?.(); await monitoredRun; expect(settled).toBe(true); expect(registry.httpRoutes).toHaveLength(0); expect(runtime.log).toHaveBeenCalledWith( expect.stringContaining("Zalo provider stopped mode=webhook"), ); }); });