* 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
214 lines
6.3 KiB
TypeScript
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"),
|
|
);
|
|
});
|
|
});
|