mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 08:11:09 +00:00
170 lines
5.3 KiB
TypeScript
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"),
|
|
);
|
|
});
|
|
});
|