mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:00:42 +00:00
test(extensions): add discord and telegram coverage
This commit is contained in:
63
extensions/discord/src/draft-chunking.test.ts
Normal file
63
extensions/discord/src/draft-chunking.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveDiscordDraftStreamingChunking } from "./draft-chunking.js";
|
||||
|
||||
describe("resolveDiscordDraftStreamingChunking", () => {
|
||||
it("returns sane defaults when discord draft chunking is unset", () => {
|
||||
expect(resolveDiscordDraftStreamingChunking(undefined)).toEqual({
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph",
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps requested draft chunk sizes to the resolved text limit", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
textChunkLimit: 500,
|
||||
draftChunk: {
|
||||
minChars: 900,
|
||||
maxChars: 1200,
|
||||
breakPreference: "sentence",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveDiscordDraftStreamingChunking(cfg)).toEqual({
|
||||
minChars: 500,
|
||||
maxChars: 500,
|
||||
breakPreference: "sentence",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers account draft chunking over channel defaults", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
discord: {
|
||||
draftChunk: {
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph",
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
draftChunk: {
|
||||
minChars: 25,
|
||||
maxChars: 75,
|
||||
breakPreference: "newline",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveDiscordDraftStreamingChunking(cfg, "ops")).toEqual({
|
||||
minChars: 25,
|
||||
maxChars: 75,
|
||||
breakPreference: "newline",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,35 @@
|
||||
import { Message } from "@buape/carbon";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDiscordInboundJob, materializeDiscordInboundJob } from "./inbound-job.js";
|
||||
import {
|
||||
buildDiscordInboundJob,
|
||||
materializeDiscordInboundJob,
|
||||
resolveDiscordInboundJobQueueKey,
|
||||
} from "./inbound-job.js";
|
||||
import { createBaseDiscordMessageContext } from "./message-handler.test-harness.js";
|
||||
|
||||
describe("buildDiscordInboundJob", () => {
|
||||
it("prefers route session key, then base session key, then channel id for queueing", async () => {
|
||||
const routed = await createBaseDiscordMessageContext({
|
||||
route: { sessionKey: "agent:main:discord:direct:routed" },
|
||||
baseSessionKey: "agent:main:discord:direct:base",
|
||||
messageChannelId: "channel-routed",
|
||||
});
|
||||
const baseOnly = await createBaseDiscordMessageContext({
|
||||
route: { sessionKey: "" },
|
||||
baseSessionKey: "agent:main:discord:direct:base-only",
|
||||
messageChannelId: "channel-base",
|
||||
});
|
||||
const channelFallback = await createBaseDiscordMessageContext({
|
||||
route: { sessionKey: " " },
|
||||
baseSessionKey: " ",
|
||||
messageChannelId: "channel-fallback",
|
||||
});
|
||||
|
||||
expect(resolveDiscordInboundJobQueueKey(routed)).toBe("agent:main:discord:direct:routed");
|
||||
expect(resolveDiscordInboundJobQueueKey(baseOnly)).toBe("agent:main:discord:direct:base-only");
|
||||
expect(resolveDiscordInboundJobQueueKey(channelFallback)).toBe("channel-fallback");
|
||||
});
|
||||
|
||||
it("keeps live runtime references out of the payload", async () => {
|
||||
const ctx = await createBaseDiscordMessageContext({
|
||||
message: {
|
||||
|
||||
26
extensions/discord/src/send.typing.test.ts
Normal file
26
extensions/discord/src/send.typing.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { RequestClient } from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveDiscordRestMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
resolveDiscordRest: resolveDiscordRestMock,
|
||||
}));
|
||||
|
||||
import { sendTypingDiscord } from "./send.typing.js";
|
||||
|
||||
describe("sendTypingDiscord", () => {
|
||||
it("sends a typing event to the resolved Discord channel route", async () => {
|
||||
const post = vi.fn(async () => undefined);
|
||||
resolveDiscordRestMock.mockReturnValue({
|
||||
post,
|
||||
} as unknown as RequestClient);
|
||||
|
||||
const result = await sendTypingDiscord("12345", { accountId: "ops" });
|
||||
|
||||
expect(resolveDiscordRestMock).toHaveBeenCalledWith({ accountId: "ops" });
|
||||
expect(post).toHaveBeenCalledWith(Routes.channelTyping("12345"));
|
||||
expect(result).toEqual({ ok: true, channelId: "12345" });
|
||||
});
|
||||
});
|
||||
@@ -98,4 +98,99 @@ describe("TelegramPollingSession", () => {
|
||||
expect(computeBackoffMock).toHaveBeenCalledTimes(1);
|
||||
expect(sleepWithAbortMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("forces a restart when polling stalls without getUpdates activity", async () => {
|
||||
const abort = new AbortController();
|
||||
const botStop = vi.fn(async () => undefined);
|
||||
const firstRunnerStop = vi.fn(async () => undefined);
|
||||
const secondRunnerStop = vi.fn(async () => undefined);
|
||||
const bot = {
|
||||
api: {
|
||||
deleteWebhook: vi.fn(async () => true),
|
||||
getUpdates: vi.fn(async () => []),
|
||||
config: { use: vi.fn() },
|
||||
},
|
||||
stop: botStop,
|
||||
};
|
||||
createTelegramBotMock.mockReturnValue(bot);
|
||||
|
||||
let firstTaskResolve: (() => void) | undefined;
|
||||
const firstTask = new Promise<void>((resolve) => {
|
||||
firstTaskResolve = resolve;
|
||||
});
|
||||
let cycle = 0;
|
||||
runMock.mockImplementation(() => {
|
||||
cycle += 1;
|
||||
if (cycle === 1) {
|
||||
return {
|
||||
task: () => firstTask,
|
||||
stop: async () => {
|
||||
await firstRunnerStop();
|
||||
firstTaskResolve?.();
|
||||
},
|
||||
isRunning: () => true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
task: async () => {
|
||||
abort.abort();
|
||||
},
|
||||
stop: secondRunnerStop,
|
||||
isRunning: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
let watchdog: (() => void) | undefined;
|
||||
const setIntervalSpy = vi.spyOn(globalThis, "setInterval").mockImplementation((fn) => {
|
||||
watchdog = fn as () => void;
|
||||
return 1 as unknown as ReturnType<typeof setInterval>;
|
||||
});
|
||||
const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval").mockImplementation(() => {});
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
void Promise.resolve().then(() => (fn as () => void)());
|
||||
return 1 as unknown as ReturnType<typeof setTimeout>;
|
||||
});
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => {});
|
||||
const dateNowSpy = vi
|
||||
.spyOn(Date, "now")
|
||||
.mockImplementationOnce(() => 0)
|
||||
.mockImplementation(() => 120_001);
|
||||
|
||||
const log = vi.fn();
|
||||
const session = new TelegramPollingSession({
|
||||
token: "tok",
|
||||
config: {},
|
||||
accountId: "default",
|
||||
runtime: undefined,
|
||||
proxyFetch: undefined,
|
||||
abortSignal: abort.signal,
|
||||
runnerOptions: {},
|
||||
getLastUpdateId: () => null,
|
||||
persistUpdateId: async () => undefined,
|
||||
log,
|
||||
telegramTransport: undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
const runPromise = session.runUntilAbort();
|
||||
for (let attempt = 0; attempt < 20 && !watchdog; attempt += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
expect(watchdog).toBeTypeOf("function");
|
||||
watchdog?.();
|
||||
await runPromise;
|
||||
|
||||
expect(runMock).toHaveBeenCalledTimes(2);
|
||||
expect(firstRunnerStop).toHaveBeenCalledTimes(1);
|
||||
expect(botStop).toHaveBeenCalled();
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("Polling stall detected"));
|
||||
expect(log).toHaveBeenCalledWith(expect.stringContaining("polling stall detected"));
|
||||
} finally {
|
||||
setIntervalSpy.mockRestore();
|
||||
clearIntervalSpy.mockRestore();
|
||||
setTimeoutSpy.mockRestore();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
dateNowSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user