openclaw/extensions/zalo/src/monitor.lifecycle.test.ts
darkamenosa 67b2e81360
Zalo: fix provider lifecycle restarts (#39892)
* Zalo: fix provider lifecycle restarts

* Zalo: add typing indicators, smart webhook cleanup, and API type fixes

* fix review

* add allow list test secrect

* Zalo: bound webhook cleanup during shutdown

* Zalo: bound typing chat action timeout

* Zalo: use plugin-safe abort helper import
2026-03-08 22:33:18 +07:00

214 lines
6.3 KiB
TypeScript

import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
import { setActivePluginRegistry } from "../../../src/plugins/runtime.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 (importOriginal) => {
const actual = await importOriginal<typeof import("./api.js")>();
return {
...actual,
deleteWebhook: deleteWebhookMock,
getWebhookInfo: getWebhookInfoMock,
getUpdates: getUpdatesMock,
setWebhook: setWebhookMock,
};
});
vi.mock("./runtime.js", () => ({
getZaloRuntime: () => ({
logging: {
shouldLogVerbose: () => false,
},
}),
}));
async function waitForPollingLoopStart(): Promise<void> {
await vi.waitFor(() => expect(getUpdatesMock).toHaveBeenCalledTimes(1));
}
describe("monitorZaloProvider lifecycle", () => {
afterEach(() => {
vi.clearAllMocks();
setActivePluginRegistry(createEmptyPluginRegistry());
});
it("stays alive in polling mode until abort", async () => {
const { monitorZaloProvider } = await import("./monitor.js");
const abort = new AbortController();
const runtime = {
log: vi.fn<(message: string) => void>(),
error: vi.fn<(message: string) => void>(),
};
const account = {
accountId: "default",
config: {},
} as unknown as ResolvedZaloAccount;
const config = {} as OpenClawConfig;
let settled = false;
const run = monitorZaloProvider({
token: "test-token",
account,
config,
runtime,
abortSignal: abort.signal,
}).then(() => {
settled = true;
});
await waitForPollingLoopStart();
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
expect(deleteWebhookMock).not.toHaveBeenCalled();
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
expect(settled).toBe(false);
abort.abort();
await run;
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 { monitorZaloProvider } = await import("./monitor.js");
const abort = new AbortController();
const runtime = {
log: vi.fn<(message: string) => void>(),
error: vi.fn<(message: string) => void>(),
};
const account = {
accountId: "default",
config: {},
} as unknown as ResolvedZaloAccount;
const config = {} as OpenClawConfig;
const run = monitorZaloProvider({
token: "test-token",
account,
config,
runtime,
abortSignal: abort.signal,
});
await waitForPollingLoopStart();
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 { monitorZaloProvider } = await import("./monitor.js");
const abort = new AbortController();
const runtime = {
log: vi.fn<(message: string) => void>(),
error: vi.fn<(message: string) => void>(),
};
const account = {
accountId: "default",
config: {},
} as unknown as ResolvedZaloAccount;
const config = {} as OpenClawConfig;
const run = monitorZaloProvider({
token: "test-token",
account,
config,
runtime,
abortSignal: abort.signal,
});
await waitForPollingLoopStart();
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: "" } });
}),
);
const { monitorZaloProvider } = await import("./monitor.js");
const abort = new AbortController();
const runtime = {
log: vi.fn<(message: string) => void>(),
error: vi.fn<(message: string) => void>(),
};
const account = {
accountId: "default",
config: {},
} as unknown as ResolvedZaloAccount;
const config = {} as OpenClawConfig;
let settled = false;
const run = monitorZaloProvider({
token: "test-token",
account,
config,
runtime,
abortSignal: abort.signal,
useWebhook: true,
webhookUrl: "https://example.com/hooks/zalo",
webhookSecret: "supersecret", // pragma: allowlist secret
}).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 run;
expect(settled).toBe(true);
expect(registry.httpRoutes).toHaveLength(0);
expect(runtime.log).toHaveBeenCalledWith(
expect.stringContaining("Zalo provider stopped mode=webhook"),
);
});
});