mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 18:00:22 +00:00
refactor(test): dedupe shared test helpers
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../../../../src/runtime.js";
|
||||
import { createRuntimeEnv } from "../../../../test/helpers/extensions/runtime-env.js";
|
||||
|
||||
const { resolveDiscordChannelAllowlistMock, resolveDiscordUserAllowlistMock } = vi.hoisted(() => ({
|
||||
resolveDiscordChannelAllowlistMock: vi.fn(
|
||||
@@ -35,7 +36,7 @@ import { resolveDiscordAllowlistConfig } from "./provider.allowlist.js";
|
||||
|
||||
describe("resolveDiscordAllowlistConfig", () => {
|
||||
it("canonicalizes resolved user names to ids in runtime config", async () => {
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv;
|
||||
const runtime = createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv;
|
||||
const result = await resolveDiscordAllowlistConfig({
|
||||
token: "token",
|
||||
allowFrom: ["Alice", "111", "*"],
|
||||
@@ -69,7 +70,7 @@ describe("resolveDiscordAllowlistConfig", () => {
|
||||
channelName: "missing-room",
|
||||
},
|
||||
]);
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv;
|
||||
const runtime = createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv;
|
||||
|
||||
await resolveDiscordAllowlistConfig({
|
||||
token: "token",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { FeishuMessageEvent } from "./bot.js";
|
||||
import {
|
||||
@@ -99,16 +100,6 @@ vi.mock("../../../src/infra/outbound/session-binding-service.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessageEvent }) {
|
||||
const runtime = createRuntimeEnv();
|
||||
await handleFeishuMessage({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { monitorSingleAccount } from "./monitor.account.js";
|
||||
import { setFeishuRuntime } from "./runtime.js";
|
||||
@@ -139,14 +140,6 @@ function createLifecycleAccount(): ResolvedFeishuAccount {
|
||||
} as unknown as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createTopicEvent(messageId: string) {
|
||||
return {
|
||||
sender: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { monitorSingleAccount } from "./monitor.account.js";
|
||||
import { setFeishuRuntime } from "./runtime.js";
|
||||
@@ -133,14 +134,6 @@ function createLifecycleAccount(): ResolvedFeishuAccount {
|
||||
} as unknown as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createBotMenuEvent(params: { eventKey: string; timestamp: string }) {
|
||||
return {
|
||||
event_key: params.eventKey,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { monitorSingleAccount } from "./monitor.account.js";
|
||||
import { setFeishuRuntime } from "./runtime.js";
|
||||
@@ -159,14 +160,6 @@ function createLifecycleAccount(accountId: "account-A" | "account-B"): ResolvedF
|
||||
} as unknown as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createBroadcastEvent(messageId: string) {
|
||||
return {
|
||||
sender: {
|
||||
@@ -190,7 +183,7 @@ async function setupLifecycleMonitor(accountId: "account-A" | "account-B") {
|
||||
});
|
||||
createEventDispatcherMock.mockReturnValueOnce({ register });
|
||||
|
||||
const runtime = createRuntimeEnv();
|
||||
const runtime = createRuntimeEnv({ throwOnExit: false });
|
||||
runtimesByAccount.set(accountId, runtime);
|
||||
|
||||
await monitorSingleAccount({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { resetProcessedFeishuCardActionTokensForTests } from "./card-action.js";
|
||||
import { createFeishuCardInteractionEnvelope } from "./card-interaction.js";
|
||||
@@ -135,14 +136,6 @@ function createLifecycleAccount(): ResolvedFeishuAccount {
|
||||
} as unknown as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createCardActionEvent(params: {
|
||||
token: string;
|
||||
action: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
resolveInboundDebounceMs,
|
||||
} from "../../../src/auto-reply/inbound-debounce.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
|
||||
import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
|
||||
import * as dedup from "./dedup.js";
|
||||
@@ -172,11 +173,7 @@ async function setupDebounceMonitor(params?: {
|
||||
await monitorSingleAccount({
|
||||
cfg: buildDebounceConfig(),
|
||||
account: buildDebounceAccount(),
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv,
|
||||
botOpenIdSource: {
|
||||
kind: "prefetched",
|
||||
botOpenId: params?.botOpenId ?? "ou_bot",
|
||||
@@ -452,11 +449,7 @@ describe("monitorSingleAccount lifecycle", () => {
|
||||
await monitorSingleAccount({
|
||||
cfg: buildDebounceConfig(),
|
||||
account: buildDebounceAccount(),
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv,
|
||||
botOpenIdSource: {
|
||||
kind: "prefetched",
|
||||
botOpenId: "ou_bot",
|
||||
@@ -493,11 +486,7 @@ describe("monitorSingleAccount lifecycle", () => {
|
||||
monitorSingleAccount({
|
||||
cfg: buildDebounceConfig(),
|
||||
account: buildDebounceAccount(),
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as RuntimeEnv,
|
||||
botOpenIdSource: {
|
||||
kind: "prefetched",
|
||||
botOpenId: "ou_bot",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { monitorSingleAccount } from "./monitor.account.js";
|
||||
import { setFeishuRuntime } from "./runtime.js";
|
||||
@@ -140,14 +141,6 @@ function createLifecycleAccount(): ResolvedFeishuAccount {
|
||||
} as unknown as ResolvedFeishuAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv(): RuntimeEnv {
|
||||
return {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
} as RuntimeEnv;
|
||||
}
|
||||
|
||||
function createTextEvent(messageId: string) {
|
||||
return {
|
||||
sender: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { ClawdbotConfig } from "../runtime-api.js";
|
||||
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
|
||||
|
||||
@@ -134,7 +135,7 @@ describe("Feishu monitor startup preflight", () => {
|
||||
});
|
||||
|
||||
const abortController = new AbortController();
|
||||
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
||||
const runtime = createRuntimeEnv({ throwOnExit: false });
|
||||
const monitorPromise = monitorFeishuProvider({
|
||||
config: buildMultiAccountWebsocketConfig(["alpha", "beta"]),
|
||||
runtime,
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { RuntimeEnv } from "../runtime-api.js";
|
||||
import { matrixPlugin } from "./channel.js";
|
||||
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
||||
import { resolveMatrixConfigForAccount } from "./matrix/client/config.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
import { installMatrixTestRuntime } from "./test-runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
describe("matrix directory", () => {
|
||||
const runtimeEnv: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn((code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
}),
|
||||
};
|
||||
const runtimeEnv: RuntimeEnv = createRuntimeEnv();
|
||||
|
||||
beforeEach(() => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
} as PluginRuntime);
|
||||
installMatrixTestRuntime();
|
||||
});
|
||||
|
||||
it("lists peers and groups from config", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
|
||||
const resolveMatrixTargetsMock = vi.hoisted(() => vi.fn(async () => []));
|
||||
|
||||
@@ -19,11 +20,7 @@ describe("matrix resolver adapter", () => {
|
||||
accountId: "ops",
|
||||
inputs: ["Alice"],
|
||||
kind: "user",
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
},
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }),
|
||||
});
|
||||
|
||||
expect(resolveMatrixTargetsMock).toHaveBeenCalledWith({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
||||
import type { RuntimeEnv } from "../runtime-api.js";
|
||||
|
||||
const verificationMocks = vi.hoisted(() => ({
|
||||
bootstrapMatrixVerification: vi.fn(),
|
||||
@@ -10,7 +10,7 @@ vi.mock("./matrix/actions/verification.js", () => ({
|
||||
}));
|
||||
|
||||
import { matrixPlugin } from "./channel.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
import { installMatrixTestRuntime } from "./test-runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
describe("matrix setup post-write bootstrap", () => {
|
||||
@@ -30,11 +30,7 @@ describe("matrix setup post-write bootstrap", () => {
|
||||
log.mockClear();
|
||||
error.mockClear();
|
||||
exit.mockClear();
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env, homeDir) => (homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
} as PluginRuntime);
|
||||
installMatrixTestRuntime();
|
||||
});
|
||||
|
||||
it("bootstraps verification for newly added encrypted accounts", async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { resolveMatrixAccountStorageRoot } from "../../../runtime-api.js";
|
||||
import { setMatrixRuntime } from "../../runtime.js";
|
||||
import { installMatrixTestRuntime } from "../../test-runtime.js";
|
||||
|
||||
const createBackupArchiveMock = vi.hoisted(() =>
|
||||
vi.fn(async (_params: unknown) => ({
|
||||
@@ -67,10 +67,8 @@ describe("matrix client storage paths", () => {
|
||||
const stateDir = path.join(homeDir, ".openclaw");
|
||||
fs.mkdirSync(stateDir, { recursive: true });
|
||||
tempDirs.push(homeDir);
|
||||
setMatrixRuntime({
|
||||
config: {
|
||||
loadConfig: () => cfg,
|
||||
},
|
||||
installMatrixTestRuntime({
|
||||
cfg,
|
||||
logging: {
|
||||
getChildLogger: () => ({
|
||||
info: () => {},
|
||||
@@ -78,10 +76,8 @@ describe("matrix client storage paths", () => {
|
||||
error: () => {},
|
||||
}),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => stateDir,
|
||||
},
|
||||
} as never);
|
||||
stateDir,
|
||||
});
|
||||
return stateDir;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { setMatrixRuntime } from "../runtime.js";
|
||||
import { installMatrixTestRuntime } from "../test-runtime.js";
|
||||
import {
|
||||
credentialsMatchConfig,
|
||||
loadMatrixCredentials,
|
||||
@@ -30,14 +30,7 @@ describe("matrix credentials storage", () => {
|
||||
): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-matrix-creds-"));
|
||||
tempDirs.push(dir);
|
||||
setMatrixRuntime({
|
||||
config: {
|
||||
loadConfig: () => cfg,
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => dir,
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime({ cfg, stateDir: dir });
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { setMatrixRuntime } from "../../runtime.js";
|
||||
import { installMatrixMonitorTestRuntime } from "../../test-runtime.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import {
|
||||
createMatrixHandlerTestHarness,
|
||||
@@ -9,22 +9,10 @@ import type { MatrixRawEvent } from "./types.js";
|
||||
|
||||
describe("createMatrixRoomMessageHandler inbound body formatting", () => {
|
||||
beforeEach(() => {
|
||||
setMatrixRuntime({
|
||||
channel: {
|
||||
mentions: {
|
||||
matchesMentionPatterns: () => false,
|
||||
},
|
||||
media: {
|
||||
saveMediaBuffer: vi.fn(),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => "/tmp",
|
||||
},
|
||||
} as never);
|
||||
installMatrixMonitorTestRuntime({
|
||||
matchesMentionPatterns: () => false,
|
||||
saveMediaBuffer: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it("records thread metadata for group thread messages", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "../../../runtime-api.js";
|
||||
import { setMatrixRuntime } from "../../runtime.js";
|
||||
import { installMatrixMonitorTestRuntime } from "../../test-runtime.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import type { MatrixRawEvent } from "./types.js";
|
||||
import { EventType } from "./types.js";
|
||||
@@ -143,23 +143,7 @@ function createImageEvent(content: Record<string, unknown>): MatrixRawEvent {
|
||||
describe("createMatrixRoomMessageHandler media failures", () => {
|
||||
beforeEach(() => {
|
||||
downloadMatrixMediaMock.mockReset();
|
||||
setMatrixRuntime({
|
||||
channel: {
|
||||
mentions: {
|
||||
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
|
||||
patterns.some((pattern) => pattern.test(text)),
|
||||
},
|
||||
media: {
|
||||
saveMediaBuffer: vi.fn(),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => "/tmp",
|
||||
},
|
||||
} as unknown as PluginRuntime);
|
||||
installMatrixMonitorTestRuntime();
|
||||
});
|
||||
|
||||
it("replaces bare image filenames with an unavailable marker when unencrypted download fails", async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
__testing as sessionBindingTesting,
|
||||
registerSessionBindingAdapter,
|
||||
} from "../../../../../src/infra/outbound/session-binding-service.js";
|
||||
import { setMatrixRuntime } from "../../runtime.js";
|
||||
import { installMatrixMonitorTestRuntime } from "../../test-runtime.js";
|
||||
import { createMatrixRoomMessageHandler } from "./handler.js";
|
||||
import {
|
||||
createMatrixHandlerTestHarness,
|
||||
@@ -26,23 +26,7 @@ vi.mock("../send.js", () => ({
|
||||
|
||||
beforeEach(() => {
|
||||
sessionBindingTesting.resetSessionBindingAdaptersForTests();
|
||||
setMatrixRuntime({
|
||||
channel: {
|
||||
mentions: {
|
||||
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
|
||||
patterns.some((pattern) => pattern.test(text)),
|
||||
},
|
||||
media: {
|
||||
saveMediaBuffer: vi.fn(),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => "/tmp",
|
||||
},
|
||||
} as never);
|
||||
installMatrixMonitorTestRuntime();
|
||||
});
|
||||
|
||||
function createReactionHarness(params?: {
|
||||
|
||||
@@ -1,29 +1,13 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "../../../runtime-api.js";
|
||||
import { setMatrixRuntime } from "../../runtime.js";
|
||||
import { installMatrixMonitorTestRuntime } from "../../test-runtime.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import { createMatrixRoomMessageHandler } from "./handler.js";
|
||||
import { EventType, type MatrixRawEvent } from "./types.js";
|
||||
|
||||
describe("createMatrixRoomMessageHandler thread root media", () => {
|
||||
it("keeps image-only thread roots visible via attachment markers", async () => {
|
||||
setMatrixRuntime({
|
||||
channel: {
|
||||
mentions: {
|
||||
matchesMentionPatterns: (text: string, patterns: RegExp[]) =>
|
||||
patterns.some((pattern) => pattern.test(text)),
|
||||
},
|
||||
media: {
|
||||
saveMediaBuffer: vi.fn(),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: () => "/tmp",
|
||||
},
|
||||
} as unknown as PluginRuntime);
|
||||
installMatrixMonitorTestRuntime();
|
||||
|
||||
const recordInboundSession = vi.fn().mockResolvedValue(undefined);
|
||||
const formatAgentEnvelope = vi
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { RuntimeEnv, WizardPrompter } from "../runtime-api.js";
|
||||
import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
import { installMatrixTestRuntime } from "./test-runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const resolveMatrixTargetsMock = vi.hoisted(() =>
|
||||
@@ -10,20 +13,9 @@ vi.mock("./resolve-targets.js", () => ({
|
||||
resolveMatrixTargets: resolveMatrixTargetsMock,
|
||||
}));
|
||||
|
||||
import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
|
||||
describe("matrix onboarding account-scoped resolution", () => {
|
||||
beforeEach(() => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
resolveMatrixTargetsMock.mockClear();
|
||||
});
|
||||
|
||||
@@ -91,7 +83,7 @@ describe("matrix onboarding account-scoped resolution", () => {
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { RuntimeEnv, WizardPrompter } from "../runtime-api.js";
|
||||
import { matrixOnboardingAdapter } from "./onboarding.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
import { installMatrixTestRuntime } from "./test-runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
vi.mock("./matrix/deps.js", () => ({
|
||||
@@ -32,15 +33,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("offers env shortcut for non-default account when scoped env vars are present", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
process.env.MATRIX_HOMESERVER = "https://matrix.env.example.org";
|
||||
process.env.MATRIX_USER_ID = "@env:example.org";
|
||||
@@ -89,7 +82,7 @@ describe("matrix onboarding", () => {
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
@@ -118,15 +111,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("promotes legacy top-level Matrix config before adding a named account", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const prompter = {
|
||||
note: vi.fn(async () => {}),
|
||||
@@ -167,7 +152,7 @@ describe("matrix onboarding", () => {
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
@@ -197,15 +182,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("includes device env var names in auth help text", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const notes: string[] = [];
|
||||
const prompter = {
|
||||
@@ -222,7 +199,7 @@ describe("matrix onboarding", () => {
|
||||
await expect(
|
||||
matrixOnboardingAdapter.configureInteractive!({
|
||||
cfg: { channels: {} } as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
@@ -241,15 +218,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("prompts for private-network access when onboarding an internal http homeserver", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const prompter = {
|
||||
note: vi.fn(async () => {}),
|
||||
@@ -284,7 +253,7 @@ describe("matrix onboarding", () => {
|
||||
|
||||
const result = await matrixOnboardingAdapter.configureInteractive!({
|
||||
cfg: {} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
@@ -336,15 +305,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("writes allowlists and room access to the selected Matrix account", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const prompter = {
|
||||
note: vi.fn(async () => {}),
|
||||
@@ -405,7 +366,7 @@ describe("matrix onboarding", () => {
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as unknown as RuntimeEnv,
|
||||
runtime: createRuntimeEnv({ throwOnExit: false }) as unknown as RuntimeEnv,
|
||||
prompter,
|
||||
options: undefined,
|
||||
accountOverrides: {},
|
||||
@@ -470,15 +431,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("reports configured when only the effective default Matrix account is configured", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const status = await matrixOnboardingAdapter.getStatus({
|
||||
cfg: {
|
||||
@@ -503,15 +456,7 @@ describe("matrix onboarding", () => {
|
||||
});
|
||||
|
||||
it("asks for defaultAccount when multiple named Matrix accounts exist", async () => {
|
||||
setMatrixRuntime({
|
||||
state: {
|
||||
resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) =>
|
||||
(homeDir ?? (() => "/tmp"))(),
|
||||
},
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
},
|
||||
} as never);
|
||||
installMatrixTestRuntime();
|
||||
|
||||
const status = await matrixOnboardingAdapter.getStatus({
|
||||
cfg: {
|
||||
|
||||
66
extensions/matrix/src/test-runtime.ts
Normal file
66
extensions/matrix/src/test-runtime.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { vi } from "vitest";
|
||||
import type { PluginRuntime } from "./runtime-api.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
|
||||
type MatrixTestRuntimeOptions = {
|
||||
cfg?: Record<string, unknown>;
|
||||
logging?: Partial<PluginRuntime["logging"]>;
|
||||
channel?: Partial<PluginRuntime["channel"]>;
|
||||
stateDir?: string;
|
||||
};
|
||||
|
||||
export function installMatrixTestRuntime(options: MatrixTestRuntimeOptions = {}): void {
|
||||
const defaultStateDirResolver: NonNullable<PluginRuntime["state"]>["resolveStateDir"] = (
|
||||
_env,
|
||||
homeDir,
|
||||
) => options.stateDir ?? (homeDir ?? (() => "/tmp"))();
|
||||
const logging: PluginRuntime["logging"] | undefined = options.logging
|
||||
? ({
|
||||
shouldLogVerbose: () => false,
|
||||
getChildLogger: () => ({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
}),
|
||||
...options.logging,
|
||||
} as PluginRuntime["logging"])
|
||||
: undefined;
|
||||
|
||||
setMatrixRuntime({
|
||||
config: {
|
||||
loadConfig: () => options.cfg ?? {},
|
||||
},
|
||||
...(options.channel ? { channel: options.channel as PluginRuntime["channel"] } : {}),
|
||||
...(logging ? { logging } : {}),
|
||||
state: {
|
||||
resolveStateDir: defaultStateDirResolver,
|
||||
},
|
||||
} as PluginRuntime);
|
||||
}
|
||||
|
||||
type MatrixMonitorTestRuntimeOptions = Pick<MatrixTestRuntimeOptions, "cfg" | "stateDir"> & {
|
||||
matchesMentionPatterns?: (text: string, patterns: RegExp[]) => boolean;
|
||||
saveMediaBuffer?: NonNullable<NonNullable<PluginRuntime["channel"]>["media"]>["saveMediaBuffer"];
|
||||
};
|
||||
|
||||
export function installMatrixMonitorTestRuntime(
|
||||
options: MatrixMonitorTestRuntimeOptions = {},
|
||||
): void {
|
||||
installMatrixTestRuntime({
|
||||
cfg: options.cfg,
|
||||
stateDir: options.stateDir,
|
||||
channel: {
|
||||
mentions: {
|
||||
buildMentionRegexes: () => [],
|
||||
matchesMentionPatterns:
|
||||
options.matchesMentionPatterns ??
|
||||
((text: string, patterns: RegExp[]) => patterns.some((pattern) => pattern.test(text))),
|
||||
matchesMentionWithExplicit: () => false,
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia: vi.fn(),
|
||||
saveMediaBuffer: options.saveMediaBuffer ?? vi.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import "./monitor-inbox.test-harness.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { monitorWebInbox } from "./inbound.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildNotifyMessageUpsert,
|
||||
expectPairingPromptSent,
|
||||
getAuthDir,
|
||||
getSock,
|
||||
installWebMonitorInboxUnitTestHooks,
|
||||
mockLoadConfig,
|
||||
settleInboundWork,
|
||||
startInboxMonitor,
|
||||
upsertPairingRequestMock,
|
||||
waitForMessageCalls,
|
||||
} from "./monitor-inbox.test-harness.js";
|
||||
|
||||
const nowSeconds = (offsetMs = 0) => Math.floor((Date.now() + offsetMs) / 1000);
|
||||
@@ -29,26 +29,8 @@ function createAllowListConfig(allowFrom: string[]) {
|
||||
}
|
||||
|
||||
async function openInboxMonitor(onMessage = vi.fn()) {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
return { onMessage, listener, sock: getSock() };
|
||||
}
|
||||
|
||||
async function settleInboundWork() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
|
||||
async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, count: number) {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(onMessage).toHaveBeenCalledTimes(count);
|
||||
},
|
||||
{ timeout: 2_000, interval: 5 },
|
||||
);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
return { onMessage, listener, sock };
|
||||
}
|
||||
|
||||
async function expectOutboundDmSkipsPairing(params: {
|
||||
@@ -67,13 +49,7 @@ async function expectOutboundDmSkipsPairing(params: {
|
||||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
const sock = getSock();
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
|
||||
try {
|
||||
sock.ev.emit("messages.upsert", {
|
||||
@@ -112,16 +88,12 @@ describe("web monitor inbox", () => {
|
||||
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: { id: "auth1", fromMe: false, remoteJid: "999@s.whatsapp.net" },
|
||||
message: { conversation: "authorized message" },
|
||||
messageTimestamp: nowSeconds(60_000),
|
||||
},
|
||||
],
|
||||
};
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "auth1",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
text: "authorized message",
|
||||
timestamp: nowSeconds(60_000),
|
||||
});
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await waitForMessageCalls(onMessage, 1);
|
||||
@@ -146,16 +118,12 @@ describe("web monitor inbox", () => {
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
// Message from self (sock.user.id is "123@s.whatsapp.net" in mock)
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: { id: "self1", fromMe: false, remoteJid: "123@s.whatsapp.net" },
|
||||
message: { conversation: "self message" },
|
||||
messageTimestamp: nowSeconds(60_000),
|
||||
},
|
||||
],
|
||||
};
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "self1",
|
||||
remoteJid: "123@s.whatsapp.net",
|
||||
text: "self message",
|
||||
timestamp: nowSeconds(60_000),
|
||||
});
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await waitForMessageCalls(onMessage, 1);
|
||||
@@ -178,20 +146,12 @@ describe("web monitor inbox", () => {
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
// Message from someone else should be blocked
|
||||
const upsertBlocked = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "no-config-1",
|
||||
fromMe: false,
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "ping" },
|
||||
messageTimestamp: nowSeconds(),
|
||||
},
|
||||
],
|
||||
};
|
||||
const upsertBlocked = buildNotifyMessageUpsert({
|
||||
id: "no-config-1",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
text: "ping",
|
||||
timestamp: nowSeconds(),
|
||||
});
|
||||
|
||||
sock.ev.emit("messages.upsert", upsertBlocked);
|
||||
await vi.waitFor(
|
||||
@@ -203,20 +163,12 @@ describe("web monitor inbox", () => {
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
expectPairingPromptSent(sock, "999@s.whatsapp.net", "+999");
|
||||
|
||||
const upsertBlockedAgain = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "no-config-1b",
|
||||
fromMe: false,
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "ping again" },
|
||||
messageTimestamp: nowSeconds(),
|
||||
},
|
||||
],
|
||||
};
|
||||
const upsertBlockedAgain = buildNotifyMessageUpsert({
|
||||
id: "no-config-1b",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
text: "ping again",
|
||||
timestamp: nowSeconds(),
|
||||
});
|
||||
|
||||
sock.ev.emit("messages.upsert", upsertBlockedAgain);
|
||||
await settleInboundWork();
|
||||
@@ -224,20 +176,12 @@ describe("web monitor inbox", () => {
|
||||
expect(sock.sendMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Message from self should be allowed
|
||||
const upsertSelf = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: "no-config-2",
|
||||
fromMe: false,
|
||||
remoteJid: "123@s.whatsapp.net",
|
||||
},
|
||||
message: { conversation: "self ping" },
|
||||
messageTimestamp: nowSeconds(),
|
||||
},
|
||||
],
|
||||
};
|
||||
const upsertSelf = buildNotifyMessageUpsert({
|
||||
id: "no-config-2",
|
||||
remoteJid: "123@s.whatsapp.net",
|
||||
text: "self ping",
|
||||
timestamp: nowSeconds(),
|
||||
});
|
||||
|
||||
sock.ev.emit("messages.upsert", upsertSelf);
|
||||
await waitForMessageCalls(onMessage, 1);
|
||||
@@ -312,13 +256,7 @@ describe("web monitor inbox", () => {
|
||||
});
|
||||
|
||||
it("normalizes participant phone numbers to JIDs in sendReaction", async () => {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(),
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
});
|
||||
const sock = getSock();
|
||||
const { listener, sock } = await startInboxMonitor(vi.fn());
|
||||
|
||||
await listener.sendReaction("12345@g.us", "msg123", "👍", false, "+6421000000");
|
||||
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
import "./monitor-inbox.test-harness.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { monitorWebInbox } from "./inbound.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
getAuthDir,
|
||||
getSock,
|
||||
installWebMonitorInboxUnitTestHooks,
|
||||
settleInboundWork,
|
||||
startInboxMonitor,
|
||||
waitForMessageCalls,
|
||||
} from "./monitor-inbox.test-harness.js";
|
||||
|
||||
describe("append upsert handling (#20952)", () => {
|
||||
installWebMonitorInboxUnitTestHooks();
|
||||
type InboxOnMessage = NonNullable<Parameters<typeof monitorWebInbox>[0]["onMessage"]>;
|
||||
|
||||
async function settleInboundWork() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
|
||||
async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, count: number) {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(onMessage).toHaveBeenCalledTimes(count);
|
||||
},
|
||||
{ timeout: 2_000, interval: 5 },
|
||||
);
|
||||
}
|
||||
|
||||
async function startInboxMonitor(onMessage: InboxOnMessage) {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
});
|
||||
return { listener, sock: getSock() };
|
||||
}
|
||||
|
||||
it("processes recent append messages (within 60s of connect)", async () => {
|
||||
const onMessage = vi.fn(async () => {});
|
||||
|
||||
@@ -2,69 +2,24 @@ import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import "./monitor-inbox.test-harness.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { monitorWebInbox } from "./inbound.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
InboxOnMessage,
|
||||
buildNotifyMessageUpsert,
|
||||
getAuthDir,
|
||||
getSock,
|
||||
installWebMonitorInboxUnitTestHooks,
|
||||
startInboxMonitor,
|
||||
waitForMessageCalls,
|
||||
} from "./monitor-inbox.test-harness.js";
|
||||
|
||||
describe("web monitor inbox", () => {
|
||||
installWebMonitorInboxUnitTestHooks();
|
||||
type InboxOnMessage = NonNullable<Parameters<typeof monitorWebInbox>[0]["onMessage"]>;
|
||||
|
||||
async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, count: number) {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(onMessage).toHaveBeenCalledTimes(count);
|
||||
},
|
||||
{ timeout: 2_000, interval: 5 },
|
||||
);
|
||||
}
|
||||
|
||||
async function startInboxMonitor(onMessage: InboxOnMessage) {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
});
|
||||
return { listener, sock: getSock() };
|
||||
}
|
||||
|
||||
function buildMessageUpsert(params: {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
pushName?: string;
|
||||
participant?: string;
|
||||
}) {
|
||||
return {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: params.id,
|
||||
fromMe: false,
|
||||
remoteJid: params.remoteJid,
|
||||
participant: params.participant,
|
||||
},
|
||||
message: { conversation: params.text },
|
||||
messageTimestamp: params.timestamp,
|
||||
pushName: params.pushName,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function expectQuotedReplyContext(quotedMessage: unknown) {
|
||||
const onMessage = vi.fn(async (msg) => {
|
||||
await msg.reply("pong");
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
@@ -109,9 +64,9 @@ describe("web monitor inbox", () => {
|
||||
await msg.reply("pong");
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available");
|
||||
const upsert = buildMessageUpsert({
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "abc",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
text: "ping",
|
||||
@@ -147,8 +102,8 @@ describe("web monitor inbox", () => {
|
||||
return;
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const upsert = buildMessageUpsert({
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "abc",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
text: "ping",
|
||||
@@ -170,10 +125,10 @@ describe("web monitor inbox", () => {
|
||||
return;
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID");
|
||||
sock.signalRepository.lidMapping.getPNForLID.mockResolvedValueOnce("999:0@s.whatsapp.net");
|
||||
const upsert = buildMessageUpsert({
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "abc",
|
||||
remoteJid: "999@lid",
|
||||
text: "ping",
|
||||
@@ -201,9 +156,9 @@ describe("web monitor inbox", () => {
|
||||
JSON.stringify("1555"),
|
||||
);
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID");
|
||||
const upsert = buildMessageUpsert({
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "abc",
|
||||
remoteJid: "555@lid",
|
||||
text: "ping",
|
||||
@@ -227,10 +182,10 @@ describe("web monitor inbox", () => {
|
||||
return;
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID");
|
||||
sock.signalRepository.lidMapping.getPNForLID.mockResolvedValueOnce("444:0@s.whatsapp.net");
|
||||
const upsert = buildMessageUpsert({
|
||||
const upsert = buildNotifyMessageUpsert({
|
||||
id: "abc",
|
||||
remoteJid: "123@g.us",
|
||||
participant: "444@lid",
|
||||
@@ -264,7 +219,7 @@ describe("web monitor inbox", () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { listener, sock } = await startInboxMonitor(onMessage);
|
||||
const { listener, sock } = await startInboxMonitor(onMessage as InboxOnMessage);
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
messages: [
|
||||
|
||||
@@ -4,6 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { afterEach, beforeEach, expect, vi } from "vitest";
|
||||
import { monitorWebInbox } from "./inbound.js";
|
||||
|
||||
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -47,6 +48,10 @@ export type MockSock = {
|
||||
user: { id: string };
|
||||
};
|
||||
|
||||
const sessionState = vi.hoisted(() => ({
|
||||
sock: undefined as MockSock | undefined,
|
||||
}));
|
||||
|
||||
function createResolvedMock() {
|
||||
return vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
@@ -71,6 +76,7 @@ function createMockSock(): MockSock {
|
||||
}
|
||||
|
||||
const sock: MockSock = createMockSock();
|
||||
sessionState.sock = sock;
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/media-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/media-runtime")>();
|
||||
@@ -117,7 +123,12 @@ vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("./session.js", () => ({
|
||||
createWaSocket: vi.fn().mockResolvedValue(sock),
|
||||
createWaSocket: vi.fn().mockImplementation(async () => {
|
||||
if (!sessionState.sock) {
|
||||
throw new Error("mock WhatsApp socket not initialized");
|
||||
}
|
||||
return sessionState.sock;
|
||||
}),
|
||||
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
|
||||
getStatusCode: vi.fn(() => 500),
|
||||
}));
|
||||
@@ -126,6 +137,57 @@ export function getSock(): MockSock {
|
||||
return sock;
|
||||
}
|
||||
|
||||
export type InboxOnMessage = NonNullable<Parameters<typeof monitorWebInbox>[0]["onMessage"]>;
|
||||
|
||||
export async function settleInboundWork() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
|
||||
export async function waitForMessageCalls(onMessage: ReturnType<typeof vi.fn>, count: number) {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(onMessage).toHaveBeenCalledTimes(count);
|
||||
},
|
||||
{ timeout: 2_000, interval: 5 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function startInboxMonitor(onMessage: InboxOnMessage) {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
});
|
||||
return { listener, sock: getSock() };
|
||||
}
|
||||
|
||||
export function buildNotifyMessageUpsert(params: {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
pushName?: string;
|
||||
participant?: string;
|
||||
}) {
|
||||
return {
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
key: {
|
||||
id: params.id,
|
||||
fromMe: false,
|
||||
remoteJid: params.remoteJid,
|
||||
participant: params.participant,
|
||||
},
|
||||
message: { conversation: params.text },
|
||||
messageTimestamp: params.timestamp,
|
||||
pushName: params.pushName,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function expectPairingPromptSent(sock: MockSock, jid: string, senderE164: string) {
|
||||
expect(sock.sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(sock.sendMessage).toHaveBeenCalledWith(jid, {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
|
||||
@@ -48,13 +49,6 @@ const TEST_CONFIG = {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
function createRuntimeEnv() {
|
||||
return {
|
||||
log: vi.fn<(message: string) => void>(),
|
||||
error: vi.fn<(message: string) => void>(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("Zalo polling image handling", () => {
|
||||
const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
|
||||
const recordInboundSessionMock = vi.fn(async () => undefined);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
|
||||
@@ -39,13 +40,6 @@ const TEST_ACCOUNT = {
|
||||
|
||||
const TEST_CONFIG = {} as OpenClawConfig;
|
||||
|
||||
function createLifecycleRuntime() {
|
||||
return {
|
||||
log: vi.fn<(message: string) => void>(),
|
||||
error: vi.fn<(message: string) => void>(),
|
||||
};
|
||||
}
|
||||
|
||||
async function startLifecycleMonitor(
|
||||
options: {
|
||||
useWebhook?: boolean;
|
||||
@@ -55,7 +49,7 @@ async function startLifecycleMonitor(
|
||||
) {
|
||||
const { monitorZaloProvider } = await import("./monitor.js");
|
||||
const abort = new AbortController();
|
||||
const runtime = createLifecycleRuntime();
|
||||
const runtime = createRuntimeEnv();
|
||||
const run = monitorZaloProvider({
|
||||
token: "test-token",
|
||||
account: TEST_ACCOUNT,
|
||||
|
||||
@@ -1,141 +1,24 @@
|
||||
import { createServer, type RequestListener } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
import { clearZaloWebhookSecurityStateForTest, monitorZaloProvider } from "./monitor.js";
|
||||
|
||||
const setWebhookMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const deleteWebhookMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const getWebhookInfoMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const getUpdatesMock = vi.hoisted(() => vi.fn(() => new Promise(() => {})));
|
||||
const sendChatActionMock = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const sendMessageMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ ok: true, result: { message_id: "pairing-zalo-1" } })),
|
||||
);
|
||||
const sendPhotoMock = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const getZaloRuntimeMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./api.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./api.js")>();
|
||||
return {
|
||||
...actual,
|
||||
deleteWebhook: deleteWebhookMock,
|
||||
getUpdates: getUpdatesMock,
|
||||
getWebhookInfo: getWebhookInfoMock,
|
||||
sendChatAction: sendChatActionMock,
|
||||
sendMessage: sendMessageMock,
|
||||
sendPhoto: sendPhotoMock,
|
||||
setWebhook: setWebhookMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getZaloRuntime: getZaloRuntimeMock,
|
||||
}));
|
||||
|
||||
async function withServer(handler: RequestListener, fn: (baseUrl: string) => Promise<void>) {
|
||||
const server = createServer(handler);
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
const address = server.address() as AddressInfo | null;
|
||||
if (!address) {
|
||||
throw new Error("missing server address");
|
||||
}
|
||||
try {
|
||||
await fn(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
function createLifecycleConfig(): OpenClawConfig {
|
||||
return {
|
||||
channels: {
|
||||
zalo: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
"acct-zalo-pairing": {
|
||||
enabled: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret", // pragma: allowlist secret
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createLifecycleAccount(): ResolvedZaloAccount {
|
||||
return {
|
||||
accountId: "acct-zalo-pairing",
|
||||
enabled: true,
|
||||
token: "zalo-token",
|
||||
tokenSource: "config",
|
||||
config: {
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret", // pragma: allowlist secret
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
},
|
||||
} as ResolvedZaloAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv() {
|
||||
return {
|
||||
log: vi.fn<(message: string) => void>(),
|
||||
error: vi.fn<(message: string) => void>(),
|
||||
};
|
||||
}
|
||||
|
||||
function createTextUpdate(messageId: string) {
|
||||
return {
|
||||
event_name: "message.text.received",
|
||||
message: {
|
||||
from: { id: "user-unauthorized", name: "Unauthorized User" },
|
||||
chat: { id: "dm-pairing-1", chat_type: "PRIVATE" as const },
|
||||
message_id: messageId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
text: "hello from zalo",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function settleAsyncWork(): Promise<void> {
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
async function postWebhookUpdate(params: {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
secret: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
return await fetch(`${params.baseUrl}${params.path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-bot-api-secret-token": params.secret,
|
||||
},
|
||||
body: JSON.stringify(params.payload),
|
||||
});
|
||||
}
|
||||
import {
|
||||
createLifecycleAccount,
|
||||
createLifecycleConfig,
|
||||
createTextUpdate,
|
||||
getZaloRuntimeMock,
|
||||
postWebhookUpdate,
|
||||
resetLifecycleTestState,
|
||||
sendMessageMock,
|
||||
settleAsyncWork,
|
||||
startWebhookLifecycleMonitor,
|
||||
} from "../../../test/helpers/extensions/zalo-lifecycle.js";
|
||||
import { withServer } from "../../../test/helpers/http-test-server.js";
|
||||
import type { PluginRuntime } from "../runtime-api.js";
|
||||
|
||||
describe("Zalo pairing lifecycle", () => {
|
||||
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
|
||||
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearZaloWebhookSecurityStateForTest();
|
||||
resetLifecycleTestState();
|
||||
|
||||
getZaloRuntimeMock.mockReturnValue(
|
||||
createPluginRuntimeMock({
|
||||
@@ -156,38 +39,32 @@ describe("Zalo pairing lifecycle", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
resetLifecycleTestState();
|
||||
});
|
||||
|
||||
it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry);
|
||||
const abort = new AbortController();
|
||||
const runtime = createRuntimeEnv();
|
||||
const run = monitorZaloProvider({
|
||||
token: "zalo-token",
|
||||
account: createLifecycleAccount(),
|
||||
config: createLifecycleConfig(),
|
||||
runtime,
|
||||
abortSignal: abort.signal,
|
||||
useWebhook: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret",
|
||||
const { abort, route, run } = await startWebhookLifecycleMonitor({
|
||||
account: createLifecycleAccount({
|
||||
accountId: "acct-zalo-pairing",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
}),
|
||||
config: createLifecycleConfig({
|
||||
accountId: "acct-zalo-pairing",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setWebhookMock).toHaveBeenCalledTimes(1);
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
});
|
||||
const route = registry.httpRoutes[0];
|
||||
if (!route) {
|
||||
throw new Error("missing plugin HTTP route");
|
||||
}
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const payload = createTextUpdate(`zalo-pairing-${Date.now()}`);
|
||||
const payload = createTextUpdate({
|
||||
messageId: `zalo-pairing-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
});
|
||||
const first = await postWebhookUpdate({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
@@ -239,34 +116,28 @@ describe("Zalo pairing lifecycle", () => {
|
||||
it("does not emit a second pairing reply when replay arrives after the first send fails", async () => {
|
||||
sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed"));
|
||||
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry);
|
||||
const abort = new AbortController();
|
||||
const runtime = createRuntimeEnv();
|
||||
const run = monitorZaloProvider({
|
||||
token: "zalo-token",
|
||||
account: createLifecycleAccount(),
|
||||
config: createLifecycleConfig(),
|
||||
runtime,
|
||||
abortSignal: abort.signal,
|
||||
useWebhook: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret",
|
||||
const { abort, route, run, runtime } = await startWebhookLifecycleMonitor({
|
||||
account: createLifecycleAccount({
|
||||
accountId: "acct-zalo-pairing",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
}),
|
||||
config: createLifecycleConfig({
|
||||
accountId: "acct-zalo-pairing",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: [],
|
||||
}),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setWebhookMock).toHaveBeenCalledTimes(1);
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
});
|
||||
const route = registry.httpRoutes[0];
|
||||
if (!route) {
|
||||
throw new Error("missing plugin HTTP route");
|
||||
}
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const payload = createTextUpdate(`zalo-pairing-retry-${Date.now()}`);
|
||||
const payload = createTextUpdate({
|
||||
messageId: `zalo-pairing-retry-${Date.now()}`,
|
||||
userId: "user-unauthorized",
|
||||
userName: "Unauthorized User",
|
||||
chatId: "dm-pairing-1",
|
||||
});
|
||||
const first = await postWebhookUpdate({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
|
||||
@@ -1,132 +1,18 @@
|
||||
import { createServer, type RequestListener } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import type { ResolvedZaloAccount } from "./accounts.js";
|
||||
import { clearZaloWebhookSecurityStateForTest, monitorZaloProvider } from "./monitor.js";
|
||||
|
||||
const setWebhookMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const deleteWebhookMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const getWebhookInfoMock = vi.hoisted(() => vi.fn(async () => ({ ok: true, result: { url: "" } })));
|
||||
const getUpdatesMock = vi.hoisted(() => vi.fn(() => new Promise(() => {})));
|
||||
const sendChatActionMock = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const sendMessageMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ ok: true, result: { message_id: "reply-zalo-1" } })),
|
||||
);
|
||||
const sendPhotoMock = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
||||
const getZaloRuntimeMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./api.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./api.js")>();
|
||||
return {
|
||||
...actual,
|
||||
deleteWebhook: deleteWebhookMock,
|
||||
getUpdates: getUpdatesMock,
|
||||
getWebhookInfo: getWebhookInfoMock,
|
||||
sendChatAction: sendChatActionMock,
|
||||
sendMessage: sendMessageMock,
|
||||
sendPhoto: sendPhotoMock,
|
||||
setWebhook: setWebhookMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getZaloRuntime: getZaloRuntimeMock,
|
||||
}));
|
||||
|
||||
async function withServer(handler: RequestListener, fn: (baseUrl: string) => Promise<void>) {
|
||||
const server = createServer(handler);
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
const address = server.address() as AddressInfo | null;
|
||||
if (!address) {
|
||||
throw new Error("missing server address");
|
||||
}
|
||||
try {
|
||||
await fn(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
function createLifecycleConfig(): OpenClawConfig {
|
||||
return {
|
||||
channels: {
|
||||
zalo: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
"acct-zalo-lifecycle": {
|
||||
enabled: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret", // pragma: allowlist secret
|
||||
dmPolicy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createLifecycleAccount(): ResolvedZaloAccount {
|
||||
return {
|
||||
accountId: "acct-zalo-lifecycle",
|
||||
enabled: true,
|
||||
token: "zalo-token",
|
||||
tokenSource: "config",
|
||||
config: {
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret", // pragma: allowlist secret
|
||||
dmPolicy: "open",
|
||||
},
|
||||
} as ResolvedZaloAccount;
|
||||
}
|
||||
|
||||
function createRuntimeEnv() {
|
||||
return {
|
||||
log: vi.fn<(message: string) => void>(),
|
||||
error: vi.fn<(message: string) => void>(),
|
||||
};
|
||||
}
|
||||
|
||||
function createTextUpdate(messageId: string) {
|
||||
return {
|
||||
event_name: "message.text.received",
|
||||
message: {
|
||||
from: { id: "user-1", name: "User One" },
|
||||
chat: { id: "dm-chat-1", chat_type: "PRIVATE" as const },
|
||||
message_id: messageId,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
text: "hello from zalo",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function settleAsyncWork(): Promise<void> {
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
async function postWebhookUpdate(params: {
|
||||
baseUrl: string;
|
||||
path: string;
|
||||
secret: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
return await fetch(`${params.baseUrl}${params.path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-bot-api-secret-token": params.secret,
|
||||
},
|
||||
body: JSON.stringify(params.payload),
|
||||
});
|
||||
}
|
||||
import {
|
||||
createLifecycleAccount,
|
||||
createLifecycleConfig,
|
||||
createTextUpdate,
|
||||
getZaloRuntimeMock,
|
||||
postWebhookUpdate,
|
||||
resetLifecycleTestState,
|
||||
sendMessageMock,
|
||||
settleAsyncWork,
|
||||
startWebhookLifecycleMonitor,
|
||||
} from "../../../test/helpers/extensions/zalo-lifecycle.js";
|
||||
import { withServer } from "../../../test/helpers/http-test-server.js";
|
||||
import type { PluginRuntime } from "../runtime-api.js";
|
||||
|
||||
describe("Zalo reply-once lifecycle", () => {
|
||||
const finalizeInboundContextMock = vi.fn((ctx: Record<string, unknown>) => ctx);
|
||||
@@ -142,8 +28,7 @@ describe("Zalo reply-once lifecycle", () => {
|
||||
const dispatchReplyWithBufferedBlockDispatcherMock = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearZaloWebhookSecurityStateForTest();
|
||||
resetLifecycleTestState();
|
||||
|
||||
getZaloRuntimeMock.mockReturnValue(
|
||||
createPluginRuntimeMock({
|
||||
@@ -168,7 +53,7 @@ describe("Zalo reply-once lifecycle", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
resetLifecycleTestState();
|
||||
});
|
||||
|
||||
it("routes one accepted webhook event to one visible reply across duplicate replay", async () => {
|
||||
@@ -178,32 +63,26 @@ describe("Zalo reply-once lifecycle", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry);
|
||||
const abort = new AbortController();
|
||||
const runtime = createRuntimeEnv();
|
||||
const run = monitorZaloProvider({
|
||||
token: "zalo-token",
|
||||
account: createLifecycleAccount(),
|
||||
config: createLifecycleConfig(),
|
||||
runtime,
|
||||
abortSignal: abort.signal,
|
||||
useWebhook: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret",
|
||||
const { abort, route, run } = await startWebhookLifecycleMonitor({
|
||||
account: createLifecycleAccount({
|
||||
accountId: "acct-zalo-lifecycle",
|
||||
dmPolicy: "open",
|
||||
}),
|
||||
config: createLifecycleConfig({
|
||||
accountId: "acct-zalo-lifecycle",
|
||||
dmPolicy: "open",
|
||||
}),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(setWebhookMock).toHaveBeenCalledTimes(1));
|
||||
expect(registry.httpRoutes).toHaveLength(1);
|
||||
const route = registry.httpRoutes[0];
|
||||
if (!route) {
|
||||
throw new Error("missing plugin HTTP route");
|
||||
}
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const payload = createTextUpdate(`zalo-replay-${Date.now()}`);
|
||||
const payload = createTextUpdate({
|
||||
messageId: `zalo-replay-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
});
|
||||
const first = await postWebhookUpdate({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
@@ -265,31 +144,26 @@ describe("Zalo reply-once lifecycle", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry);
|
||||
const abort = new AbortController();
|
||||
const runtime = createRuntimeEnv();
|
||||
const run = monitorZaloProvider({
|
||||
token: "zalo-token",
|
||||
account: createLifecycleAccount(),
|
||||
config: createLifecycleConfig(),
|
||||
runtime,
|
||||
abortSignal: abort.signal,
|
||||
useWebhook: true,
|
||||
webhookUrl: "https://example.com/hooks/zalo",
|
||||
webhookSecret: "supersecret",
|
||||
const { abort, route, run, runtime } = await startWebhookLifecycleMonitor({
|
||||
account: createLifecycleAccount({
|
||||
accountId: "acct-zalo-lifecycle",
|
||||
dmPolicy: "open",
|
||||
}),
|
||||
config: createLifecycleConfig({
|
||||
accountId: "acct-zalo-lifecycle",
|
||||
dmPolicy: "open",
|
||||
}),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(setWebhookMock).toHaveBeenCalledTimes(1));
|
||||
const route = registry.httpRoutes[0];
|
||||
if (!route) {
|
||||
throw new Error("missing plugin HTTP route");
|
||||
}
|
||||
|
||||
await withServer(
|
||||
(req, res) => route.handler(req, res),
|
||||
async (baseUrl) => {
|
||||
const payload = createTextUpdate(`zalo-retry-${Date.now()}`);
|
||||
const payload = createTextUpdate({
|
||||
messageId: `zalo-retry-${Date.now()}`,
|
||||
userId: "user-1",
|
||||
userName: "User One",
|
||||
chatId: "dm-chat-1",
|
||||
});
|
||||
const first = await postWebhookUpdate({
|
||||
baseUrl,
|
||||
path: "/hooks/zalo",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createServer, type RequestListener } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import type { RequestListener } from "node:http";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
||||
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
|
||||
import { withServer } from "../../../test/helpers/http-test-server.js";
|
||||
import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
|
||||
import {
|
||||
clearZaloWebhookSecurityStateForTest,
|
||||
@@ -14,22 +14,6 @@ import {
|
||||
} from "./monitor.js";
|
||||
import type { ResolvedZaloAccount } from "./types.js";
|
||||
|
||||
async function withServer(handler: RequestListener, fn: (baseUrl: string) => Promise<void>) {
|
||||
const server = createServer(handler);
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
const address = server.address() as AddressInfo | null;
|
||||
if (!address) {
|
||||
throw new Error("missing server address");
|
||||
}
|
||||
try {
|
||||
await fn(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_ACCOUNT: ResolvedZaloAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user