From 93a1f5b3fa04521f5816c5670c31b0e7ed6c2289 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 04:53:44 +0100 Subject: [PATCH] test(discord,zalo): trim slow extension tests --- ...messages-mentionpatterns-match.e2e.test.ts | 229 ------------ ...ends-status-replies-responseprefix.test.ts | 113 ------ .../src/monitor.tool-result.test-harness.ts | 74 ---- .../src/monitor.tool-result.test-helpers.ts | 330 ------------------ .../discord/src/monitor/native-command-ui.ts | 6 +- .../native-command.model-picker.test.ts | 1 + .../zalo/src/monitor.image.polling.test.ts | 4 +- .../src/monitor.pairing.lifecycle.test.ts | 4 +- .../src/monitor.polling.media-reply.test.ts | 4 +- .../src/monitor.reply-once.lifecycle.test.ts | 4 +- 10 files changed, 14 insertions(+), 755 deletions(-) delete mode 100644 extensions/discord/src/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts delete mode 100644 extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts delete mode 100644 extensions/discord/src/monitor.tool-result.test-harness.ts delete mode 100644 extensions/discord/src/monitor.tool-result.test-helpers.ts diff --git a/extensions/discord/src/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts b/extensions/discord/src/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts deleted file mode 100644 index 6ebe2cf7be2..00000000000 --- a/extensions/discord/src/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.e2e.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { ChannelType, MessageType } from "@buape/carbon"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { dispatchMock } from "./monitor.tool-result.test-harness.js"; -import { - captureNextDispatchCtx, - type Config, - createGuildHandler, - createGuildMessageEvent, - createGuildTextClient, - createMentionRequiredGuildConfig, - createThreadChannel, - createThreadClient, - createThreadEvent, - resetDiscordToolResultHarness, -} from "./monitor.tool-result.test-helpers.js"; - -beforeEach(() => { - resetDiscordToolResultHarness(); -}); - -async function createHandler(cfg: Config) { - return createGuildHandler({ cfg }); -} - -function createOpenGuildConfig( - channels: Record, - extra: Partial = {}, - discordOverrides: Partial["discord"]> = {}, -): Config { - const base = createMentionRequiredGuildConfig(); - const cfg: Config = { - ...base, - ...extra, - channels: { - ...base.channels, - ...extra.channels, - discord: { - ...base.channels?.discord, - ...extra.channels?.discord, - dm: { enabled: true, policy: "open" }, - groupPolicy: "open", - guilds: { - "*": { - requireMention: false, - channels, - }, - }, - ...discordOverrides, - }, - }, - }; - return cfg; -} - -describe("discord tool result dispatch", () => { - it("accepts guild messages when mentionPatterns match", async () => { - const cfg = createMentionRequiredGuildConfig({ - messages: { - inbound: { debounceMs: 0 }, - groupChat: { mentionPatterns: ["\\bopenclaw\\b"] }, - }, - } as Partial); - - const handler = await createHandler(cfg); - const client = createGuildTextClient(); - - await handler(createGuildMessageEvent({ messageId: "m2", content: "openclaw: hello" }), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - }); - - it("accepts guild reply-to-bot messages as implicit mentions", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ WasMentioned?: boolean }>(); - const cfg = createMentionRequiredGuildConfig(); - const handler = await createHandler(cfg); - const client = createGuildTextClient(); - - await handler( - createGuildMessageEvent({ - messageId: "m3", - content: "following up", - messagePatch: { - referencedMessage: { - id: "m2", - channelId: "c1", - content: "bot reply", - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - author: { id: "bot-id", bot: true, username: "OpenClaw" }, - }, - }, - }), - client, - ); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - expect(getCapturedCtx()?.WasMentioned).toBe(true); - }); - - it("isolates thread sessions by default and injects starter context", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ - SessionKey?: string; - ParentSessionKey?: string; - ThreadStarterBody?: string; - ThreadLabel?: string; - }>(); - const cfg = createOpenGuildConfig({ p1: { allow: true } }); - - const handler = await createHandler(cfg); - const client = createThreadClient({ - fetchChannel: vi - .fn() - .mockResolvedValueOnce(createThreadChannel({ includeStarter: true })) - .mockResolvedValueOnce({ id: "p1", type: ChannelType.GuildText, name: "general" }), - }); - - await handler(createThreadEvent("m4"), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - const capturedCtx = getCapturedCtx(); - expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1"); - expect(capturedCtx?.ParentSessionKey).toBeUndefined(); - expect(capturedCtx?.ThreadStarterBody).toContain("starter message"); - expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general"); - }); - - it("skips thread starter context when disabled", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ ThreadStarterBody?: string }>(); - const cfg = createOpenGuildConfig({ - p1: { allow: true, includeThreadStarter: false }, - }); - - const handler = await createHandler(cfg); - const client = createThreadClient(); - - await handler(createThreadEvent("m7"), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - expect(getCapturedCtx()?.ThreadStarterBody).toBeUndefined(); - }); - - it("treats forum threads as distinct sessions without channel payloads", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ - SessionKey?: string; - ParentSessionKey?: string; - ThreadStarterBody?: string; - ThreadLabel?: string; - }>(); - const cfg = createOpenGuildConfig({ "forum-1": { allow: true } }); - - const fetchChannel = vi - .fn() - .mockResolvedValueOnce({ - id: "t1", - type: ChannelType.PublicThread, - name: "topic-1", - parentId: "forum-1", - }) - .mockResolvedValueOnce({ - id: "forum-1", - type: ChannelType.GuildForum, - name: "support", - }); - const restGet = vi.fn().mockResolvedValue({ - content: "starter message", - author: { id: "u1", username: "Alice", discriminator: "0001" }, - timestamp: new Date().toISOString(), - }); - const handler = await createHandler(cfg); - const client = createThreadClient({ fetchChannel, restGet }); - - await handler(createThreadEvent("m6"), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - const capturedCtx = getCapturedCtx(); - expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1"); - expect(capturedCtx?.ParentSessionKey).toBeUndefined(); - expect(capturedCtx?.ThreadStarterBody).toContain("starter message"); - expect(capturedCtx?.ThreadLabel).toContain("Discord thread #support"); - }); - - it("scopes isolated thread sessions to the routed agent", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ - SessionKey?: string; - ParentSessionKey?: string; - }>(); - const cfg = createOpenGuildConfig( - { p1: { allow: true } }, - { bindings: [{ agentId: "support", match: { channel: "discord", guildId: "g1" } }] }, - ); - - const handler = await createHandler(cfg); - const client = createThreadClient(); - - await handler(createThreadEvent("m5"), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - const capturedCtx = getCapturedCtx(); - expect(capturedCtx?.SessionKey).toBe("agent:support:discord:channel:t1"); - expect(capturedCtx?.ParentSessionKey).toBeUndefined(); - }); - - it("inherits parent thread sessions when thread.inheritParent is enabled", async () => { - const getCapturedCtx = captureNextDispatchCtx<{ - SessionKey?: string; - ParentSessionKey?: string; - }>(); - const cfg = createOpenGuildConfig( - { p1: { allow: true } }, - {}, - { thread: { inheritParent: true } }, - ); - - const handler = await createHandler(cfg); - const client = createThreadClient(); - - await handler(createThreadEvent("m8"), client); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - const capturedCtx = getCapturedCtx(); - expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1"); - expect(capturedCtx?.ParentSessionKey).toBe("agent:main:discord:channel:p1"); - }); -}); diff --git a/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts b/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts deleted file mode 100644 index 354ba2a72de..00000000000 --- a/extensions/discord/src/monitor.tool-result.sends-status-replies-responseprefix.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { MessageType } from "@buape/carbon"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { expectPairingReplyText } from "../../../test/helpers/pairing-reply.js"; -import { - dispatchMock, - sendMock, - upsertPairingRequestMock, -} from "./monitor.tool-result.test-harness.js"; -import { - BASE_CFG, - createCategoryGuildClient, - createCategoryGuildEvent, - createCategoryGuildHandler, - createDmClient, - createDmHandler, - type Config, - resetDiscordToolResultHarness, -} from "./monitor.tool-result.test-helpers.js"; - -beforeEach(() => { - resetDiscordToolResultHarness(); -}); - -describe("discord tool result dispatch", () => { - it("uses channel id allowlists for non-thread channels with categories", async () => { - let capturedCtx: { SessionKey?: string } | undefined; - dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => { - capturedCtx = ctx; - dispatcher.sendFinalReply({ text: "hi" }); - return { queuedFinal: true, counts: { final: 1 } }; - }); - - const handler = await createCategoryGuildHandler(); - const client = createCategoryGuildClient(); - - await handler( - createCategoryGuildEvent({ - messageId: "m-category", - author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" }, - }), - client, - ); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:c1"); - }); - - it("prefixes group bodies with sender label", async () => { - let capturedBody = ""; - dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => { - capturedBody = ctx.Body ?? ""; - dispatcher.sendFinalReply({ text: "ok" }); - return { queuedFinal: true, counts: { final: 1 } }; - }); - - const handler = await createCategoryGuildHandler(); - const client = createCategoryGuildClient(); - - await handler( - createCategoryGuildEvent({ - messageId: "m-prefix", - timestamp: new Date("2026-01-17T00:00:00Z").toISOString(), - author: { id: "u1", bot: false, username: "Ada", discriminator: "1234" }, - }), - client, - ); - - await vi.waitFor(() => expect(dispatchMock).toHaveBeenCalledTimes(1)); - expect(capturedBody).toContain("Ada (Ada#1234): hello"); - }); - - it("replies with pairing code and sender id when dmPolicy is pairing", async () => { - const cfg: Config = { - ...BASE_CFG, - channels: { - discord: { dm: { enabled: true, policy: "pairing", allowFrom: [] } }, - }, - }; - - const handler = await createDmHandler({ cfg }); - const client = createDmClient(); - - await handler( - { - message: { - id: "m1", - content: "hello", - channelId: "c1", - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - author: { id: "u2", bot: false, username: "Ada" }, - }, - author: { id: "u2", bot: false, username: "Ada" }, - guild_id: null, - }, - client, - ); - - expect(dispatchMock).not.toHaveBeenCalled(); - expect(upsertPairingRequestMock).toHaveBeenCalled(); - expect(sendMock).toHaveBeenCalledTimes(1); - expectPairingReplyText(String(sendMock.mock.calls[0]?.[1] ?? ""), { - channel: "discord", - idLine: "Your Discord user id: u2", - code: "PAIRCODE", - }); - }, 10000); -}); diff --git a/extensions/discord/src/monitor.tool-result.test-harness.ts b/extensions/discord/src/monitor.tool-result.test-harness.ts deleted file mode 100644 index c69173c842b..00000000000 --- a/extensions/discord/src/monitor.tool-result.test-harness.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { MockFn } from "openclaw/plugin-sdk/testing"; -import { vi } from "vitest"; - -export const sendMock: MockFn = vi.fn(); -export const reactMock: MockFn = vi.fn(); -export const updateLastRouteMock: MockFn = vi.fn(); -export const dispatchMock: MockFn = vi.fn(); -export const readAllowFromStoreMock: MockFn = vi.fn(); -export const upsertPairingRequestMock: MockFn = vi.fn(); -export const loadConfigMock: MockFn = vi.fn(); - -export const TOOL_RESULT_SESSION_STORE_PATH = `/tmp/openclaw-sessions-${process.pid}.json`; - -const sendModule = await import("./send.js"); -const replyRuntimeModule = await import("openclaw/plugin-sdk/reply-runtime"); -const conversationRuntimeModule = await import("openclaw/plugin-sdk/conversation-runtime"); -type ReadChannelAllowFromStore = typeof conversationRuntimeModule.readChannelAllowFromStore; -type UpsertChannelPairingRequest = typeof conversationRuntimeModule.upsertChannelPairingRequest; - -function createPairingStoreMocks() { - return { - readChannelAllowFromStore( - ...args: Parameters - ): ReturnType { - return readAllowFromStoreMock(...args) as ReturnType; - }, - upsertChannelPairingRequest( - ...args: Parameters - ): ReturnType { - return upsertPairingRequestMock(...args) as ReturnType; - }, - }; -} - -const pairingStoreMocks = createPairingStoreMocks(); -const configRuntimeModule = await import("openclaw/plugin-sdk/config-runtime"); - -export function installDiscordToolResultHarnessSpies() { - vi.spyOn(sendModule, "sendMessageDiscord").mockImplementation( - (...args) => sendMock(...args) as never, - ); - vi.spyOn(sendModule, "reactMessageDiscord").mockImplementation(async (...args) => { - reactMock(...args); - return { ok: true }; - }); - vi.spyOn(replyRuntimeModule, "dispatchInboundMessage").mockImplementation( - (...args) => dispatchMock(...args) as never, - ); - vi.spyOn(replyRuntimeModule, "dispatchInboundMessageWithDispatcher").mockImplementation( - (...args) => dispatchMock(...args) as never, - ); - vi.spyOn(replyRuntimeModule, "dispatchInboundMessageWithBufferedDispatcher").mockImplementation( - (...args) => dispatchMock(...args) as never, - ); - vi.spyOn(conversationRuntimeModule, "readChannelAllowFromStore").mockImplementation((...args) => - pairingStoreMocks.readChannelAllowFromStore(...args), - ); - vi.spyOn(conversationRuntimeModule, "upsertChannelPairingRequest").mockImplementation((...args) => - pairingStoreMocks.upsertChannelPairingRequest(...args), - ); - vi.spyOn(configRuntimeModule, "loadConfig").mockImplementation( - (...args) => loadConfigMock(...args) as never, - ); - vi.spyOn(configRuntimeModule, "readSessionUpdatedAt").mockImplementation(() => undefined); - vi.spyOn(configRuntimeModule, "resolveStorePath").mockImplementation( - () => TOOL_RESULT_SESSION_STORE_PATH, - ); - vi.spyOn(configRuntimeModule, "updateLastRoute").mockImplementation( - (...args) => updateLastRouteMock(...args) as never, - ); - vi.spyOn(configRuntimeModule, "resolveSessionKey").mockImplementation(vi.fn() as never); -} - -installDiscordToolResultHarnessSpies(); diff --git a/extensions/discord/src/monitor.tool-result.test-helpers.ts b/extensions/discord/src/monitor.tool-result.test-helpers.ts deleted file mode 100644 index 1dca85e7e5e..00000000000 --- a/extensions/discord/src/monitor.tool-result.test-helpers.ts +++ /dev/null @@ -1,330 +0,0 @@ -import type { Client } from "@buape/carbon"; -import { ChannelType, MessageType } from "@buape/carbon"; -import type { loadConfig } from "openclaw/plugin-sdk/config-runtime"; -import { vi } from "vitest"; -import { - dispatchMock, - installDiscordToolResultHarnessSpies, - loadConfigMock, - readAllowFromStoreMock, - sendMock, - TOOL_RESULT_SESSION_STORE_PATH, - updateLastRouteMock, - upsertPairingRequestMock, -} from "./monitor.tool-result.test-harness.js"; -import { createDiscordMessageHandler } from "./monitor/message-handler.js"; -import { __resetDiscordChannelInfoCacheForTest } from "./monitor/message-utils.js"; -import { createNoopThreadBindingManager } from "./monitor/thread-bindings.js"; - -export type Config = ReturnType; - -export const BASE_CFG: Config = { - agents: { - defaults: { - model: { primary: "anthropic/claude-opus-4-5" }, - workspace: "/tmp/openclaw", - }, - }, - messages: { - inbound: { debounceMs: 0 }, - }, - session: { store: TOOL_RESULT_SESSION_STORE_PATH }, -}; - -export const CATEGORY_GUILD_CFG = { - ...BASE_CFG, - channels: { - discord: { - dm: { enabled: true, policy: "open" }, - guilds: { - "*": { - requireMention: false, - channels: { c1: { enabled: true } }, - }, - }, - }, - }, -} satisfies Config; - -export function resetDiscordToolResultHarness() { - installDiscordToolResultHarnessSpies(); - __resetDiscordChannelInfoCacheForTest(); - sendMock.mockClear().mockResolvedValue(undefined); - updateLastRouteMock.mockClear(); - dispatchMock.mockClear().mockImplementation(async ({ dispatcher }) => { - dispatcher.sendFinalReply({ text: "hi" }); - return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; - }); - readAllowFromStoreMock.mockClear().mockResolvedValue([]); - upsertPairingRequestMock.mockClear().mockResolvedValue({ code: "PAIRCODE", created: true }); - loadConfigMock.mockClear().mockReturnValue(BASE_CFG); -} - -export function createHandlerBaseConfig( - cfg: Config, - runtimeError?: (err: unknown) => void, -): Parameters[0] { - return { - cfg, - discordConfig: cfg.channels?.discord, - accountId: "default", - token: "token", - runtime: { - log: vi.fn(), - error: runtimeError ?? vi.fn(), - exit: (code: number): never => { - throw new Error(`exit ${code}`); - }, - }, - botUserId: "bot-id", - guildHistories: new Map(), - historyLimit: 0, - mediaMaxBytes: 10_000, - textLimit: 2000, - replyToMode: "off", - dmEnabled: true, - groupDmEnabled: false, - threadBindings: createNoopThreadBindingManager("default"), - }; -} - -export async function createDmHandler(params: { - cfg: Config; - runtimeError?: (err: unknown) => void; -}) { - loadConfigMock.mockReturnValue(params.cfg); - return createDiscordMessageHandler(createHandlerBaseConfig(params.cfg, params.runtimeError)); -} - -export async function createGuildHandler(params: { - cfg: Config; - guildEntries?: Parameters[0]["guildEntries"]; - runtimeError?: (err: unknown) => void; -}) { - loadConfigMock.mockReturnValue(params.cfg); - return createDiscordMessageHandler({ - ...createHandlerBaseConfig(params.cfg, params.runtimeError), - guildEntries: - params.guildEntries ?? - (params.cfg.channels?.discord?.guilds as Parameters< - typeof createDiscordMessageHandler - >[0]["guildEntries"]), - }); -} - -export function createDmClient() { - return { - fetchChannel: vi.fn().mockResolvedValue({ - type: ChannelType.DM, - name: "dm", - }), - } as unknown as Client; -} - -export async function createCategoryGuildHandler(runtimeError?: (err: unknown) => void) { - return createGuildHandler({ - cfg: CATEGORY_GUILD_CFG, - guildEntries: { - "*": { requireMention: false, channels: { c1: { enabled: true } } }, - }, - runtimeError, - }); -} - -export function createCategoryGuildClient() { - return { - fetchChannel: vi.fn().mockResolvedValue({ - type: ChannelType.GuildText, - name: "general", - parentId: "category-1", - }), - rest: { get: vi.fn() }, - } as unknown as Client; -} - -export function createCategoryGuildEvent(params: { - messageId: string; - timestamp?: string; - author: Record; -}) { - return { - message: { - id: params.messageId, - content: "hello", - channelId: "c1", - timestamp: params.timestamp ?? new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - author: params.author, - }, - author: params.author, - member: { displayName: "Ada" }, - guild: { id: "g1", name: "Guild" }, - guild_id: "g1", - }; -} - -export function createGuildTextClient() { - return { - fetchChannel: vi.fn().mockResolvedValue({ - id: "c1", - type: ChannelType.GuildText, - name: "general", - }), - rest: { get: vi.fn() }, - } as unknown as Client; -} - -export function createGuildMessageEvent(params: { - messageId: string; - content: string; - messagePatch?: Record; - eventPatch?: Record; -}) { - const messageBase = { - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - }; - return { - message: { - id: params.messageId, - content: params.content, - channelId: "c1", - ...messageBase, - author: { id: "u1", bot: false, username: "Ada" }, - ...params.messagePatch, - }, - author: { id: "u1", bot: false, username: "Ada" }, - member: { nickname: "Ada" }, - guild: { id: "g1", name: "Guild" }, - guild_id: "g1", - ...params.eventPatch, - }; -} - -export function createThreadChannel(params: { includeStarter?: boolean; type?: ChannelType } = {}) { - return { - id: "t1", - type: params.type ?? ChannelType.PublicThread, - name: "thread-name", - parentId: params.type === ChannelType.PublicThread ? "forum-1" : "p1", - parent: { - id: params.type === ChannelType.PublicThread ? "forum-1" : "p1", - name: params.type === ChannelType.PublicThread ? "support" : "general", - }, - isThread: () => true, - ...(params.includeStarter - ? { - fetchStarterMessage: async () => ({ - content: "starter message", - author: { tag: "Alice#1", username: "Alice" }, - createdTimestamp: Date.now(), - }), - } - : {}), - }; -} - -export function createThreadClient( - params: { - fetchChannel?: ReturnType; - restGet?: ReturnType; - } = {}, -) { - return { - fetchChannel: - params.fetchChannel ?? - vi - .fn() - .mockResolvedValueOnce({ - id: "t1", - type: ChannelType.PublicThread, - name: "thread-name", - parentId: "p1", - ownerId: "owner-1", - }) - .mockResolvedValueOnce({ - id: "p1", - type: ChannelType.GuildText, - name: "general", - }), - rest: { - get: - params.restGet ?? - vi.fn().mockResolvedValue({ - content: "starter message", - author: { id: "u1", username: "Alice", discriminator: "0001" }, - timestamp: new Date().toISOString(), - }), - }, - } as unknown as Client; -} - -export function createThreadEvent(messageId: string, channelId = "t1") { - return { - message: { - id: messageId, - content: "thread hello", - channelId, - timestamp: new Date().toISOString(), - type: MessageType.Default, - attachments: [], - embeds: [], - mentionedEveryone: false, - mentionedUsers: [], - mentionedRoles: [], - author: { id: "u1", bot: false, username: "Ada" }, - }, - author: { id: "u1", bot: false, username: "Ada" }, - member: { nickname: "Ada" }, - guild: { id: "g1", name: "Guild" }, - guild_id: "g1", - }; -} - -export function createMentionRequiredGuildConfig(overrides?: Partial): Config { - return { - ...BASE_CFG, - channels: { - discord: { - dm: { enabled: true, policy: "open" }, - groupPolicy: "open", - guilds: { - "*": { - requireMention: true, - channels: { c1: { enabled: true } }, - }, - }, - }, - }, - ...overrides, - }; -} - -export function captureNextDispatchCtx< - // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test helper lets assertions ascribe captured dispatch context shape. - T extends { - SessionKey?: string; - ParentSessionKey?: string; - ThreadStarterBody?: string; - ThreadLabel?: string; - WasMentioned?: boolean; - }, ->(): () => T | undefined { - let capturedCtx: T | undefined; - dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => { - capturedCtx = ctx as T; - dispatcher.sendFinalReply({ text: "hi" }); - return { queuedFinal: true, counts: { final: 1 } }; - }); - return () => capturedCtx; -} diff --git a/extensions/discord/src/monitor/native-command-ui.ts b/extensions/discord/src/monitor/native-command-ui.ts index 2706deabba1..4f3fdae9fc1 100644 --- a/extensions/discord/src/monitor/native-command-ui.ts +++ b/extensions/discord/src/monitor/native-command-ui.ts @@ -63,6 +63,7 @@ export type DiscordCommandArgContext = { accountId: string; sessionPrefix: string; threadBindings: ThreadBindingManager; + postApplySettleMs?: number; }; export type DiscordModelPickerContext = DiscordCommandArgContext; @@ -793,7 +794,10 @@ export async function handleDiscordModelPickerInteraction(params: { return; } - await new Promise((resolve) => setTimeout(resolve, 250)); + const settleMs = ctx.postApplySettleMs ?? 250; + if (settleMs > 0) { + await new Promise((resolve) => setTimeout(resolve, settleMs)); + } const effectiveModelRef = resolveDiscordModelPickerCurrentModel({ cfg: ctx.cfg, diff --git a/extensions/discord/src/monitor/native-command.model-picker.test.ts b/extensions/discord/src/monitor/native-command.model-picker.test.ts index 9c4f1f61247..0220585feb1 100644 --- a/extensions/discord/src/monitor/native-command.model-picker.test.ts +++ b/extensions/discord/src/monitor/native-command.model-picker.test.ts @@ -77,6 +77,7 @@ function createModelPickerContext(): ModelPickerContext { accountId: "default", sessionPrefix: "discord:slash", threadBindings: createNoopThreadBindingManager("default"), + postApplySettleMs: 0, }; } diff --git a/extensions/zalo/src/monitor.image.polling.test.ts b/extensions/zalo/src/monitor.image.polling.test.ts index 52742df12ef..1d86d6bdc1b 100644 --- a/extensions/zalo/src/monitor.image.polling.test.ts +++ b/extensions/zalo/src/monitor.image.polling.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; import { createImageLifecycleCore, @@ -28,7 +28,7 @@ describe("Zalo polling image handling", () => { getZaloRuntimeMock.mockReturnValue(core); }); - afterEach(async () => { + afterAll(async () => { await resetLifecycleTestState(); }); diff --git a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts index bd5d1cad550..fe4bd053de1 100644 --- a/extensions/zalo/src/monitor.pairing.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.pairing.lifecycle.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { withServer } from "../../../test/helpers/http-test-server.js"; import { createLifecycleMonitorSetup, @@ -31,7 +31,7 @@ describe("Zalo pairing lifecycle", () => { }); }); - afterEach(async () => { + afterAll(async () => { await resetLifecycleTestState(); }); diff --git a/extensions/zalo/src/monitor.polling.media-reply.test.ts b/extensions/zalo/src/monitor.polling.media-reply.test.ts index 42357d884d4..b1a2c5a59e8 100644 --- a/extensions/zalo/src/monitor.polling.media-reply.test.ts +++ b/extensions/zalo/src/monitor.polling.media-reply.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry-empty.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; import { createRuntimeEnv } from "../../../test/helpers/plugins/runtime-env.js"; @@ -75,7 +75,7 @@ describe("Zalo polling media replies", () => { }); }); - afterEach(async () => { + afterAll(async () => { await resetLifecycleTestState(); }); diff --git a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts index 546f839670f..f8a8f7062fc 100644 --- a/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts +++ b/extensions/zalo/src/monitor.reply-once.lifecycle.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; import { withServer } from "../../../test/helpers/http-test-server.js"; import type { PluginRuntime } from "../runtime-api.js"; import { @@ -47,7 +47,7 @@ describe("Zalo reply-once lifecycle", () => { }); }); - afterEach(async () => { + afterAll(async () => { await resetLifecycleTestState(); });