import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { registerTelegramNativeCommands } from "./bot-native-commands.js"; import { createNativeCommandTestParams } from "./bot-native-commands.test-helpers.js"; // All mocks scoped to this file only — does not affect bot-native-commands.test.ts const sessionMocks = vi.hoisted(() => ({ recordSessionMetaFromInbound: vi.fn(), resolveStorePath: vi.fn(), })); const replyMocks = vi.hoisted(() => ({ dispatchReplyWithBufferedBlockDispatcher: vi.fn(async () => undefined), })); vi.mock("../config/sessions.js", () => ({ recordSessionMetaFromInbound: sessionMocks.recordSessionMetaFromInbound, resolveStorePath: sessionMocks.resolveStorePath, })); vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: vi.fn(async () => []), })); vi.mock("../auto-reply/reply/inbound-context.js", () => ({ finalizeInboundContext: vi.fn((ctx: unknown) => ctx), })); vi.mock("../auto-reply/reply/provider-dispatcher.js", () => ({ dispatchReplyWithBufferedBlockDispatcher: replyMocks.dispatchReplyWithBufferedBlockDispatcher, })); vi.mock("../channels/reply-prefix.js", () => ({ createReplyPrefixOptions: vi.fn(() => ({ onModelSelected: () => {} })), })); vi.mock("../auto-reply/skill-commands.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, listSkillCommandsForAgents: vi.fn(() => []) }; }); vi.mock("../plugins/commands.js", () => ({ getPluginCommandSpecs: vi.fn(() => []), matchPluginCommand: vi.fn(() => null), executePluginCommand: vi.fn(async () => ({ text: "ok" })), })); vi.mock("./bot/delivery.js", () => ({ deliverReplies: vi.fn(async () => ({ delivered: true })), })); function createDeferred() { let resolve!: (value: T | PromiseLike) => void; const promise = new Promise((res) => { resolve = res; }); return { promise, resolve }; } type TelegramCommandHandler = (ctx: unknown) => Promise; function buildStatusCommandContext() { return { match: "", message: { message_id: 1, date: Math.floor(Date.now() / 1000), chat: { id: 100, type: "private" as const }, from: { id: 200, username: "bob" }, }, }; } function registerAndResolveStatusHandler(cfg: OpenClawConfig): TelegramCommandHandler { const commandHandlers = new Map(); registerTelegramNativeCommands({ ...createNativeCommandTestParams({ bot: { api: { setMyCommands: vi.fn().mockResolvedValue(undefined), sendMessage: vi.fn().mockResolvedValue(undefined), }, command: vi.fn((name: string, cb: TelegramCommandHandler) => { commandHandlers.set(name, cb); }), } as unknown as Parameters[0]["bot"], cfg, allowFrom: ["*"], }), }); const handler = commandHandlers.get("status"); expect(handler).toBeTruthy(); return handler as TelegramCommandHandler; } describe("registerTelegramNativeCommands — session metadata", () => { beforeEach(() => { sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined); sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json"); replyMocks.dispatchReplyWithBufferedBlockDispatcher.mockClear().mockResolvedValue(undefined); }); it("calls recordSessionMetaFromInbound after a native slash command", async () => { const cfg: OpenClawConfig = {}; const handler = registerAndResolveStatusHandler(cfg); await handler(buildStatusCommandContext()); expect(sessionMocks.recordSessionMetaFromInbound).toHaveBeenCalledTimes(1); const call = ( sessionMocks.recordSessionMetaFromInbound.mock.calls as unknown as Array< [{ sessionKey?: string; ctx?: { OriginatingChannel?: string; Provider?: string } }] > )[0]?.[0]; expect(call?.ctx?.OriginatingChannel).toBe("telegram"); expect(call?.ctx?.Provider).toBe("telegram"); expect(call?.sessionKey).toBeDefined(); }); it("awaits session metadata persistence before dispatch", async () => { const deferred = createDeferred(); sessionMocks.recordSessionMetaFromInbound.mockReturnValue(deferred.promise); const cfg: OpenClawConfig = {}; const handler = registerAndResolveStatusHandler(cfg); const runPromise = handler(buildStatusCommandContext()); await vi.waitFor(() => { expect(sessionMocks.recordSessionMetaFromInbound).toHaveBeenCalledTimes(1); }); expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); deferred.resolve(); await runPromise; expect(replyMocks.dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1); }); });