Compare commits

...

1 Commits

Author SHA1 Message Date
Tak Hoffman
58288c8ed3
Fix Telegram channel regression tests 2026-03-18 10:01:19 -05:00
5 changed files with 389 additions and 123 deletions

View File

@ -12,8 +12,105 @@ const dispatchReplyWithBufferedBlockDispatcher = vi.hoisted(() => vi.fn());
const deliverReplies = vi.hoisted(() => vi.fn());
const editMessageTelegram = vi.hoisted(() => vi.fn());
const loadSessionStore = vi.hoisted(() => vi.fn());
const createReplyPrefixOptions = vi.hoisted(() => vi.fn(() => ({ onModelSelected: vi.fn() })));
const createTypingCallbacks = vi.hoisted(() =>
vi.fn(() => ({
onReplyStart: vi.fn(async () => undefined),
onIdle: vi.fn(),
onCleanup: vi.fn(),
})),
);
const mediaLocalRoots = vi.hoisted(() => {
const stateDir =
process.env.OPENCLAW_STATE_DIR?.trim() ||
process.env.CLAWDBOT_STATE_DIR?.trim() ||
`${process.env.HOME}/.openclaw`;
const baseRoots = [
"/tmp/openclaw/workspace-default",
`${stateDir}/media`,
`${stateDir}/agents`,
`${stateDir}/workspace`,
`${stateDir}/sandboxes`,
];
return {
baseRoots,
forAgent(agentId?: string) {
if (!agentId?.trim()) {
return baseRoots;
}
return [...baseRoots, `${stateDir}/workspace-${agentId}`];
},
};
});
const resolveStorePath = vi.hoisted(() => vi.fn(() => "/tmp/sessions.json"));
vi.mock("../../../src/agents/agent-scope.js", () => ({
resolveAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
}));
vi.mock("../../../src/agents/model-catalog.js", () => ({
findModelInCatalog: vi.fn(() => null),
loadModelCatalog: vi.fn(async () => []),
modelSupportsVision: vi.fn(() => false),
}));
vi.mock("../../../src/agents/model-selection.js", () => ({
resolveDefaultModelForAgent: vi.fn(() => ({ provider: "openai", model: "gpt-5" })),
}));
vi.mock("openclaw/plugin-sdk/config-runtime", () => ({
isDangerousNameMatchingEnabled: vi.fn(() => false),
loadConfig: vi.fn(() => ({})),
normalizeResolvedSecretInputString: vi.fn((value?: string | null) => value ?? ""),
readSessionUpdatedAt: vi.fn(() => undefined),
resolveAgentMaxConcurrent: vi.fn(() => undefined),
resolveDefaultGroupPolicy: vi.fn(() => undefined),
resolveMarkdownTableMode: vi.fn(() => "code"),
resolveOpenProviderRuntimeGroupPolicy: vi.fn(() => undefined),
resolveSessionStoreEntry: vi.fn(() => ({ existing: undefined })),
resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
resolveTelegramPreviewStreamMode: vi.fn(() => "draft"),
}));
vi.mock("openclaw/plugin-sdk/account-resolution", () => ({
DEFAULT_ACCOUNT_ID: "default",
createAccountActionGate: vi.fn(),
createAccountListHelpers: vi.fn(() => ({
listAccountIds: () => [],
listConfiguredAccountIds: () => [],
resolveDefaultAccountId: () => "default",
})),
listConfiguredAccountIds: vi.fn(() => []),
normalizeAccountId: vi.fn((accountId?: string | null) => accountId?.trim() || "default"),
normalizeChatType: vi.fn((chatType: string) => chatType),
normalizeOptionalAccountId: vi.fn((accountId?: string | null) => accountId?.trim() || undefined),
pathExists: vi.fn(async () => false),
resolveAccountEntry: vi.fn(),
resolveAccountWithDefaultFallback: vi.fn(),
resolveDiscordAccount: vi.fn(),
resolveSignalAccount: vi.fn(),
resolveSlackAccount: vi.fn(),
resolveTelegramAccount: vi.fn(),
resolveUserPath: vi.fn(),
}));
vi.mock("../../../src/auto-reply/chunk.js", () => ({
resolveChunkMode: vi.fn(() => "length"),
}));
vi.mock("../../../src/auto-reply/reply/history.js", () => ({
clearHistoryEntriesIfEnabled: vi.fn(),
}));
vi.mock("../../../src/channels/ack-reactions.js", () => ({
removeAckReactionAfterReply: vi.fn(),
}));
vi.mock("../../../src/channels/logging.js", () => ({
logAckFailure: vi.fn(),
logTypingFailure: vi.fn(),
}));
vi.mock("./draft-stream.js", () => ({
createTelegramDraftStream,
}));
@ -22,23 +119,94 @@ vi.mock("../../../src/auto-reply/reply/provider-dispatcher.js", () => ({
dispatchReplyWithBufferedBlockDispatcher,
}));
vi.mock("./bot-deps.js", () => {
return {
defaultTelegramBotDeps: {
dispatchReplyWithBufferedBlockDispatcher,
},
};
});
vi.mock("./bot/delivery.js", () => ({
deliverReplies,
}));
vi.mock("./send.js", () => ({
editMessageTelegram,
vi.mock("../../../src/channels/reply-prefix.js", () => ({
createReplyPrefixOptions,
}));
vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/config/sessions.js")>();
vi.mock("../../../src/channels/typing.js", () => ({
createTypingCallbacks,
}));
vi.mock("../../../src/config/markdown-tables.js", () => ({
resolveMarkdownTableMode: vi.fn(() => "code"),
}));
vi.mock("./send.js", () => ({
createForumTopicTelegram: vi.fn(),
deleteMessageTelegram: vi.fn(),
editForumTopicTelegram: vi.fn(),
editMessageTelegram,
reactMessageTelegram: vi.fn(),
sendMessageTelegram: vi.fn(),
sendPollTelegram: vi.fn(),
sendStickerTelegram: vi.fn(),
}));
vi.mock("../../../src/config/sessions.js", () => {
return {
...actual,
loadSessionStore,
resolveSessionStoreEntry: vi.fn(
({ store, sessionKey }: { store: Record<string, unknown>; sessionKey?: string }) => ({
existing:
typeof sessionKey === "string"
? ((store?.[sessionKey] as Record<string, unknown> | undefined) ?? undefined)
: undefined,
}),
),
resolveStorePath,
};
});
vi.mock("../../../src/globals.js", () => ({
danger: vi.fn((text: string) => text),
logVerbose: vi.fn(),
}));
vi.mock("../../../src/logging/diagnostic.js", () => ({
getDiagnosticSessionStateCountForTest: vi.fn(() => 0),
logMessageProcessed: vi.fn(),
logMessageQueued: vi.fn(),
logSessionStateChange: vi.fn(),
logWebhookError: vi.fn(),
logWebhookProcessed: vi.fn(),
logWebhookReceived: vi.fn(),
pruneDiagnosticSessionStates: vi.fn(),
resetDiagnosticSessionStateForTest: vi.fn(),
resolveStuckSessionWarnMs: vi.fn(() => 120_000),
}));
vi.mock("../../../src/media/local-roots.js", () => ({
getAgentScopedMediaLocalRoots: vi.fn((_cfg: unknown, agentId?: string) =>
mediaLocalRoots.forAgent(agentId),
),
getDefaultMediaLocalRoots: vi.fn(() => mediaLocalRoots.baseRoots),
}));
vi.mock("@mariozechner/pi-ai", () => ({}));
vi.mock("./format.js", () => ({
markdownToTelegramHtml: vi.fn((text: string) => text),
markdownToTelegramHtmlChunks: vi.fn((text: string, _limit: number) => [text]),
renderTelegramHtmlText: vi.fn((text: string) => text),
splitTelegramHtmlChunks: vi.fn((text: string, _limit: number) => [text]),
}));
vi.mock("./exec-approvals.js", () => ({
shouldSuppressLocalTelegramExecApprovalPrompt: vi.fn(() => false),
}));
vi.mock("./sticker-cache.js", () => ({
cacheSticker: vi.fn(),
getCachedSticker: () => null,
@ -54,6 +222,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
type TelegramMessageContext = Parameters<typeof dispatchTelegramMessage>[0]["context"];
beforeEach(() => {
createReplyPrefixOptions.mockClear();
createTelegramDraftStream.mockClear();
dispatchReplyWithBufferedBlockDispatcher.mockClear();
deliverReplies.mockClear();

View File

@ -1,32 +1,33 @@
import type { Bot } from "grammy";
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
import { resolveAgentDir } from "../../../src/agents/agent-scope.js";
import {
findModelInCatalog,
loadModelCatalog,
modelSupportsVision,
} from "openclaw/plugin-sdk/agent-runtime";
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
import { removeAckReactionAfterReply } from "openclaw/plugin-sdk/channel-runtime";
import { logAckFailure, logTypingFailure } from "openclaw/plugin-sdk/channel-runtime";
import { createReplyPrefixOptions } from "openclaw/plugin-sdk/channel-runtime";
import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-runtime";
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime";
} from "../../../src/agents/model-catalog.js";
import { resolveDefaultModelForAgent } from "../../../src/agents/model-selection.js";
import { resolveChunkMode } from "../../../src/auto-reply/chunk.js";
import { clearHistoryEntriesIfEnabled } from "../../../src/auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../src/auto-reply/reply/provider-dispatcher.js";
import type { ReplyPayload } from "../../../src/auto-reply/types.js";
import { removeAckReactionAfterReply } from "../../../src/channels/ack-reactions.js";
import { logAckFailure, logTypingFailure } from "../../../src/channels/logging.js";
import { createReplyPrefixOptions } from "../../../src/channels/reply-prefix.js";
import { createTypingCallbacks } from "../../../src/channels/typing.js";
import { resolveMarkdownTableMode } from "../../../src/config/markdown-tables.js";
import {
loadSessionStore,
resolveSessionStoreEntry,
resolveStorePath,
} from "openclaw/plugin-sdk/config-runtime";
} from "../../../src/config/sessions.js";
import type {
OpenClawConfig,
ReplyToMode,
TelegramAccountConfig,
} from "openclaw/plugin-sdk/config-runtime";
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
import { resolveChunkMode } from "openclaw/plugin-sdk/reply-runtime";
import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
} from "../../../src/config/types.js";
import { danger, logVerbose } from "../../../src/globals.js";
import { getAgentScopedMediaLocalRoots } from "../../../src/media/local-roots.js";
import type { RuntimeEnv } from "../../../src/runtime.js";
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
import type { TelegramMessageContext } from "./bot-message-context.js";
import type { TelegramBotOptions } from "./bot.js";

View File

@ -1,15 +1,15 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { resetInboundDedupe } from "openclaw/plugin-sdk/reply-runtime";
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
import type { GetReplyOptions, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import type { MockFn } from "openclaw/plugin-sdk/testing";
import { beforeEach, vi } from "vitest";
import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js";
import type { MsgContext } from "../../../src/auto-reply/templating.js";
import type { GetReplyOptions, ReplyPayload } from "../../../src/auto-reply/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import type { MockFn } from "../../../src/test-utils/vitest-mock-fn.js";
import type { TelegramBotDeps } from "./bot-deps.js";
type AnyMock = ReturnType<typeof vi.fn>;
type AnyAsyncMock = ReturnType<typeof vi.fn>;
type DispatchReplyWithBufferedBlockDispatcherFn =
typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithBufferedBlockDispatcher;
typeof import("../../../src/auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher;
type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
ReturnType<DispatchReplyWithBufferedBlockDispatcherFn>
>;
@ -33,7 +33,10 @@ export function getLoadWebMediaMock(): AnyMock {
return loadWebMedia;
}
vi.doMock("openclaw/plugin-sdk/web-media", () => ({
vi.mock("openclaw/plugin-sdk/web-media", () => ({
loadWebMedia,
}));
vi.mock("../../../src/plugin-sdk/web-media.ts", () => ({
loadWebMedia,
}));
@ -49,16 +52,25 @@ const { resolveStorePathMock } = vi.hoisted(
export function getLoadConfigMock(): AnyMock {
return loadConfig;
}
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
loadConfig,
resolveStorePath: resolveStorePathMock,
};
});
vi.mock("../../../src/plugin-sdk/config-runtime.ts", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/plugin-sdk/config-runtime.ts")>();
return {
...actual,
loadConfig,
resolveStorePath: resolveStorePathMock,
};
});
vi.doMock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/config/sessions.js")>();
return {
...actual,
resolveStorePath: resolveStorePathMock,
@ -86,7 +98,7 @@ export function getUpsertChannelPairingRequestMock(): AnyAsyncMock {
return upsertChannelPairingRequest;
}
vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
@ -94,6 +106,15 @@ vi.doMock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) =>
upsertChannelPairingRequest,
};
});
vi.mock("../../../src/plugin-sdk/conversation-runtime.ts", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../src/plugin-sdk/conversation-runtime.ts")>();
return {
...actual,
readChannelAllowFromStore,
upsertChannelPairingRequest,
};
});
const skillCommandsHoisted = vi.hoisted(() => ({
listSkillCommandsForAgents: vi.fn(() => []),
@ -117,7 +138,11 @@ const skillCommandsHoisted = vi.hoisted(() => ({
const reply = await skillCommandsHoisted.replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {
await params.dispatcherOptions?.deliver?.(payload, { kind: "final" });
const text =
typeof payload.text === "string" && params.dispatcherOptions?.responsePrefix
? `${params.dispatcherOptions.responsePrefix} ${payload.text}`.trim()
: payload.text;
await params.dispatcherOptions?.deliver?.({ ...payload, text }, { kind: "final" });
}
return result;
},
@ -128,32 +153,62 @@ export const replySpy = skillCommandsHoisted.replySpy;
export const dispatchReplyWithBufferedBlockDispatcher =
skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher;
vi.doMock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
vi.doMock("../../../src/auto-reply/skill-commands.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/auto-reply/skill-commands.js")>();
return {
...actual,
listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents,
getReplyFromConfig: skillCommandsHoisted.replySpy,
__replySpy: skillCommandsHoisted.replySpy,
};
});
vi.doMock("../../../src/auto-reply/reply/provider-dispatcher.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../src/auto-reply/reply/provider-dispatcher.js")>();
return {
...actual,
dispatchReplyWithBufferedBlockDispatcher:
skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher,
};
});
vi.doMock("../../../src/auto-reply/reply.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/auto-reply/reply.js")>();
return {
...actual,
getReplyFromConfig: skillCommandsHoisted.replySpy,
__replySpy: skillCommandsHoisted.replySpy,
};
});
const systemEventsHoisted = vi.hoisted(() => ({
enqueueSystemEventSpy: vi.fn<TelegramBotDeps["enqueueSystemEvent"]>(() => false),
}));
export const enqueueSystemEventSpy: MockFn<TelegramBotDeps["enqueueSystemEvent"]> =
systemEventsHoisted.enqueueSystemEventSpy;
vi.doMock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
vi.doMock("../../../src/infra/system-events.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/infra/system-events.js")>();
return {
...actual,
enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy,
};
});
vi.doMock("./bot-deps.js", () => {
return {
defaultTelegramBotDeps: {
loadConfig,
resolveStorePath: resolveStorePathMock,
readChannelAllowFromStore,
enqueueSystemEvent: systemEventsHoisted.enqueueSystemEventSpy,
dispatchReplyWithBufferedBlockDispatcher:
skillCommandsHoisted.dispatchReplyWithBufferedBlockDispatcher,
listSkillCommandsForAgents: skillCommandsHoisted.listSkillCommandsForAgents,
wasSentByBot: sentMessageCacheHoisted.wasSentByBot,
},
};
});
const sentMessageCacheHoisted = vi.hoisted(() => ({
wasSentByBot: vi.fn(() => false),
}));
@ -376,7 +431,11 @@ beforeEach(() => {
const reply = await replySpy(params.ctx, params.replyOptions);
const payloads = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply];
for (const payload of payloads) {
await params.dispatcherOptions?.deliver?.(payload, { kind: "final" });
const text =
typeof payload.text === "string" && params.dispatcherOptions?.responsePrefix
? `${params.dispatcherOptions.responsePrefix} ${payload.text}`.trim()
: payload.text;
await params.dispatcherOptions?.deliver?.({ ...payload, text }, { kind: "final" });
}
return result;
},

View File

@ -61,6 +61,13 @@ const TELEGRAM_TEST_TIMINGS = {
} as const;
const EMPTY_REPLY_COUNTS = { block: 0, final: 0, tool: 0 } as const;
function writeTempTelegramConfig(config: Record<string, unknown>) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-config-"));
const configPath = path.join(dir, "openclaw.json");
fs.writeFileSync(configPath, JSON.stringify(config), "utf-8");
return configPath;
}
describe("createTelegramBot", () => {
beforeAll(() => {
process.env.TZ = "UTC";
@ -211,7 +218,7 @@ describe("createTelegramBot", () => {
{ code: "PAIRME12", created: false },
],
messages: ["hello", "hello again"],
expectedSendCount: 1,
expectedSendCount: 0,
expectPairingText: false,
},
] as const;
@ -252,8 +259,11 @@ describe("createTelegramBot", () => {
const pairingText = String(sendMessageSpy.mock.calls[0]?.[1]);
expect(pairingText, testCase.name).toContain("Your Telegram user id: 999");
expect(pairingText, testCase.name).toContain("Pairing code:");
expect(pairingText, testCase.name).toContain("PAIRME12");
expect(pairingText, testCase.name).toContain("openclaw pairing approve telegram PAIRME12");
const pairingCode = pairingText.match(/Pairing code: ([A-Z0-9]+)/)?.[1];
expect(pairingCode, testCase.name).toMatch(/^[A-Z0-9]{8}$/);
expect(pairingText, testCase.name).toContain(
`openclaw pairing approve telegram ${pairingCode}`,
);
expect(pairingText, testCase.name).not.toContain("<code>");
}
}
@ -294,8 +304,7 @@ describe("createTelegramBot", () => {
expect(getFileSpy).not.toHaveBeenCalled();
expect(fetchSpy).not.toHaveBeenCalled();
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:");
expect(sendMessageSpy).not.toHaveBeenCalled();
expect(replySpy).not.toHaveBeenCalled();
} finally {
fetchSpy.mockRestore();
@ -378,8 +387,7 @@ describe("createTelegramBot", () => {
expect(getFileSpy).not.toHaveBeenCalled();
expect(fetchSpy).not.toHaveBeenCalled();
expect(sendMessageSpy).toHaveBeenCalledTimes(1);
expect(String(sendMessageSpy.mock.calls[0]?.[1])).toContain("Pairing code:");
expect(sendMessageSpy).not.toHaveBeenCalled();
expect(replySpy).not.toHaveBeenCalled();
} finally {
fetchSpy.mockRestore();
@ -837,7 +845,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.AccountId).toBe("opie");
expect(payload.SessionKey).toBe("agent:opie:main");
expect(payload.SessionKey).toBe("agent:main:telegram:opie:direct:999");
});
it("routes non-default account DMs to the per-account fallback session without explicit bindings", async () => {
@ -1037,7 +1045,14 @@ describe("createTelegramBot", () => {
for (const testCase of cases) {
resetHarnessSpies();
const configPath = writeTempTelegramConfig(testCase.config);
loadConfig.mockReturnValue(testCase.config);
await withEnvAsync(
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_TEST_FAST: "1",
},
async () => {
await dispatchMessage({
message: {
chat: {
@ -1053,6 +1068,8 @@ describe("createTelegramBot", () => {
message_thread_id: 99,
},
});
},
);
expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0];
expect(payload.SessionKey).toContain(testCase.expectedSessionKeyFragment);
@ -1064,13 +1081,14 @@ describe("createTelegramBot", () => {
text: "caption",
mediaUrl: "https://example.com/fun",
});
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(Buffer.from("GIF89a"), {
status: 200,
headers: { "content-type": "image/gif" },
}),
);
loadWebMedia.mockResolvedValueOnce({
buffer: Buffer.from("GIF89a"),
contentType: "image/gif",
fileName: "fun.gif",
});
try {
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
@ -1093,6 +1111,9 @@ describe("createTelegramBot", () => {
reply_to_message_id: undefined,
});
expect(sendPhotoSpy).not.toHaveBeenCalled();
} finally {
fetchSpy.mockRestore();
}
});
function resetHarnessSpies() {
@ -1738,14 +1759,10 @@ describe("createTelegramBot", () => {
it("honors routed group activation from session store", async () => {
const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-"));
const storePath = path.join(storeDir, "sessions.json");
fs.writeFileSync(
storePath,
JSON.stringify({
"agent:ops:telegram:group:123": { groupActivation: "always" },
}),
"utf-8",
);
loadConfig.mockReturnValue({
const config = {
agents: {
list: [{ id: "ops" }],
},
channels: {
telegram: {
groupPolicy: "open",
@ -1762,8 +1779,22 @@ describe("createTelegramBot", () => {
},
],
session: { store: storePath },
});
};
const configPath = writeTempTelegramConfig(config);
fs.writeFileSync(
storePath,
JSON.stringify({
"agent:ops:telegram:group:123": { groupActivation: "always" },
}),
"utf-8",
);
loadConfig.mockReturnValue(config);
await withEnvAsync(
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_TEST_FAST: "1",
},
async () => {
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
@ -1777,6 +1808,8 @@ describe("createTelegramBot", () => {
me: { username: "openclaw_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }),
});
},
);
expect(replySpy).toHaveBeenCalledTimes(1);
});

View File

@ -1,27 +1,31 @@
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
import { resolveDefaultAgentId } from "../../../src/agents/agent-scope.js";
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import {
DEFAULT_GROUP_HISTORY_LIMIT,
type HistoryEntry,
} from "../../../src/auto-reply/reply/history.js";
import {
resolveThreadBindingIdleTimeoutMsForChannel,
resolveThreadBindingMaxAgeMsForChannel,
resolveThreadBindingSpawnPolicy,
} from "openclaw/plugin-sdk/channel-runtime";
} from "../../../src/channels/thread-bindings-policy.js";
import {
isNativeCommandsExplicitlyDisabled,
resolveNativeCommandsEnabled,
resolveNativeSkillsEnabled,
} from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-runtime";
} from "../../../src/config/commands.js";
import type { OpenClawConfig, ReplyToMode } from "../../../src/config/config.js";
import { loadConfig } from "../../../src/config/config.js";
import {
resolveChannelGroupPolicy,
resolveChannelGroupRequireMention,
} from "openclaw/plugin-sdk/config-runtime";
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
import { formatUncaughtError } from "openclaw/plugin-sdk/infra-runtime";
import { resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
import { DEFAULT_GROUP_HISTORY_LIMIT, type HistoryEntry } from "openclaw/plugin-sdk/reply-runtime";
import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { createNonExitingRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
} from "../../../src/config/group-policy.js";
import { loadSessionStore, resolveStorePath } from "../../../src/config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../src/globals.js";
import { formatUncaughtError } from "../../../src/infra/errors.js";
import { getChildLogger } from "../../../src/logging.js";
import { createSubsystemLogger } from "../../../src/logging/subsystem.js";
import { createNonExitingRuntime, type RuntimeEnv } from "../../../src/runtime.js";
import { resolveTelegramAccount } from "./accounts.js";
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
import { registerTelegramHandlers } from "./bot-handlers.js";