test(extensions): add discord and telegram coverage

This commit is contained in:
Vincent Koc
2026-03-22 15:47:36 -07:00
parent 5c8e1275a0
commit 82508e3931
4 changed files with 211 additions and 1 deletions

View 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",
});
});
});

View File

@@ -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: {

View 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" });
});
});

View File

@@ -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();
}
});
});