Files
openclaw/extensions/zalo/src/monitor.lifecycle.test.ts
2026-04-03 19:49:43 +01:00

170 lines
5.3 KiB
TypeScript

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<typeof import("./api.js")>("./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"),
);
});
});