From 51da1f70fa4db03b43e4aaf769ac280775a22570 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 21:49:20 +0100 Subject: [PATCH] test: share msteams message handler fixtures --- .../message-handler.authz.test.ts | 84 ++----------------- .../message-handler.test-support.ts | 63 +++++++++----- .../message-handler.thread-parent.test.ts | 34 ++++---- 3 files changed, 67 insertions(+), 114 deletions(-) diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index fdc8fb0e9d3..060105049b4 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it, vi } from "vitest"; -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "../../runtime-api.js"; -import type { MSTeamsConversationStore } from "../conversation-store.js"; +import type { OpenClawConfig } from "../../runtime-api.js"; import type { GraphThreadMessage } from "../graph-thread.js"; -import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js"; -import { setMSTeamsRuntime } from "../runtime.js"; import { _resetThreadParentContextCachesForTest } from "../thread-parent-context.js"; import { createMSTeamsMessageHandler } from "./message-handler.js"; +import { createMessageHandlerDeps } from "./message-handler.test-support.js"; type HandlerInput = Parameters>[0]; type TestThreadUser = { @@ -79,81 +77,17 @@ describe("msteams monitor handler authz", () => { const readAllowFromStore = vi.fn(async () => ["attacker-aad"]); const upsertPairingRequest = vi.fn(async () => null); const recordInboundSession = vi.fn(async () => undefined); - setMSTeamsRuntime({ - logging: { shouldLogVerbose: () => false }, - system: { enqueueSystemEvent: vi.fn() }, - channel: { - debounce: { - resolveInboundDebounceMs: () => 0, - createInboundDebouncer: (params: { - onFlush: (entries: T[]) => Promise; - }): { enqueue: (entry: T) => Promise } => ({ - enqueue: async (entry: T) => { - await params.onFlush([entry]); - }, - }), - }, - pairing: { - readAllowFromStore, - upsertPairingRequest, - }, - text: { - hasControlCommand: () => false, - }, - routing: { - resolveAgentRoute: ({ peer }: { peer: { kind: string; id: string } }) => ({ - sessionKey: `msteams:${peer.kind}:${peer.id}`, - agentId: "default", - accountId: "default", - }), - }, - reply: { - formatAgentEnvelope: ({ body }: { body: string }) => body, - finalizeInboundContext: >(ctx: T) => ctx, - }, - session: { - recordInboundSession, - }, - }, - } as unknown as PluginRuntime); - const conversationStore = { - get: vi.fn(async () => null), - upsert: vi.fn(async () => undefined), - list: vi.fn(async () => []), - remove: vi.fn(async () => false), - findPreferredDmByUserId: vi.fn(async () => null), - findByUserId: vi.fn(async () => null), - } satisfies MSTeamsConversationStore; - - const deps: MSTeamsMessageHandlerDeps = { - cfg, - runtime: { error: vi.fn() } as unknown as RuntimeEnv, - appId: "test-app", - adapter: {} as MSTeamsMessageHandlerDeps["adapter"], - tokenProvider: { - getAccessToken: vi.fn(async () => "token"), - }, - textLimit: 4000, - mediaMaxBytes: 1024 * 1024, - conversationStore, - pollStore: { - recordVote: vi.fn(async () => null), - } as unknown as MSTeamsMessageHandlerDeps["pollStore"], - log: { - info: vi.fn(), - debug: vi.fn(), - error: vi.fn(), - } as unknown as MSTeamsMessageHandlerDeps["log"], - }; - - return { - conversationStore, - deps, + return createMessageHandlerDeps(cfg, { readAllowFromStore, upsertPairingRequest, recordInboundSession, - }; + resolveAgentRoute: vi.fn(({ peer }: { peer: { kind: string; id: string } }) => ({ + sessionKey: `msteams:${peer.kind}:${peer.id}`, + agentId: "default", + accountId: "default", + })), + }); } function resetThreadMocks() { diff --git a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts index 383c92a80b1..ea255ce74e1 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.test-support.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.test-support.ts @@ -5,17 +5,33 @@ import { setMSTeamsRuntime } from "../runtime.js"; export const channelConversationId = "19:general@thread.tacv2"; -export function createMessageHandlerDeps(cfg: OpenClawConfig) { - const enqueueSystemEvent = vi.fn(); - const recordInboundSession = vi.fn(async (_params: { sessionKey: string }) => undefined); - const resolveAgentRoute = vi.fn(({ peer }: { peer: { kind: string; id: string } }) => ({ - sessionKey: `agent:main:msteams:${peer.kind}:${peer.id}`, - agentId: "main", - accountId: "default", - mainSessionKey: "agent:main:main", - lastRoutePolicy: "session" as const, - matchedBy: "default" as const, - })); +type MessageHandlerDepsOptions = { + enqueueSystemEvent?: ReturnType; + readAllowFromStore?: ReturnType; + upsertPairingRequest?: ReturnType; + recordInboundSession?: ReturnType; + resolveAgentRoute?: ReturnType; +}; + +export function createMessageHandlerDeps( + cfg: OpenClawConfig, + options: MessageHandlerDepsOptions = {}, +) { + const enqueueSystemEvent = options.enqueueSystemEvent ?? vi.fn(); + const readAllowFromStore = options.readAllowFromStore ?? vi.fn(async () => []); + const upsertPairingRequest = options.upsertPairingRequest ?? vi.fn(async () => null); + const recordInboundSession = + options.recordInboundSession ?? vi.fn(async (_params: { sessionKey: string }) => undefined); + const resolveAgentRoute = + options.resolveAgentRoute ?? + vi.fn(({ peer }: { peer: { kind: string; id: string } }) => ({ + sessionKey: `agent:main:msteams:${peer.kind}:${peer.id}`, + agentId: "main", + accountId: "default", + mainSessionKey: "agent:main:main", + lastRoutePolicy: "session" as const, + matchedBy: "default" as const, + })); setMSTeamsRuntime({ logging: { shouldLogVerbose: () => false }, @@ -32,8 +48,8 @@ export function createMessageHandlerDeps(cfg: OpenClawConfig) { }), }, pairing: { - readAllowFromStore: vi.fn(async () => []), - upsertPairingRequest: vi.fn(async () => null), + readAllowFromStore, + upsertPairingRequest, }, text: { hasControlCommand: () => false, @@ -51,6 +67,15 @@ export function createMessageHandlerDeps(cfg: OpenClawConfig) { }, } as unknown as PluginRuntime); + const conversationStore = { + get: vi.fn(async () => null), + upsert: vi.fn(async () => undefined), + list: vi.fn(async () => []), + remove: vi.fn(async () => false), + findPreferredDmByUserId: vi.fn(async () => null), + findByUserId: vi.fn(async () => null), + } satisfies MSTeamsMessageHandlerDeps["conversationStore"]; + const deps: MSTeamsMessageHandlerDeps = { cfg, runtime: { error: vi.fn() } as unknown as RuntimeEnv, @@ -61,14 +86,7 @@ export function createMessageHandlerDeps(cfg: OpenClawConfig) { }, textLimit: 4000, mediaMaxBytes: 1024 * 1024, - conversationStore: { - get: vi.fn(async () => null), - upsert: vi.fn(async () => undefined), - list: vi.fn(async () => []), - remove: vi.fn(async () => false), - findPreferredDmByUserId: vi.fn(async () => null), - findByUserId: vi.fn(async () => null), - } satisfies MSTeamsMessageHandlerDeps["conversationStore"], + conversationStore, pollStore: { recordVote: vi.fn(async () => null), } as unknown as MSTeamsMessageHandlerDeps["pollStore"], @@ -80,8 +98,11 @@ export function createMessageHandlerDeps(cfg: OpenClawConfig) { }; return { + conversationStore, deps, enqueueSystemEvent, + readAllowFromStore, + upsertPairingRequest, recordInboundSession, resolveAgentRoute, }; diff --git a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts index dd2dc006738..a0291fd2ba5 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.thread-parent.test.ts @@ -49,6 +49,8 @@ vi.mock("../reply-dispatcher.js", () => ({ })); describe("msteams thread parent context injection", () => { + type MessageHandler = ReturnType; + function findParentSystemEventCall( mock: ReturnType, ): [string, { sessionKey: string; contextKey?: string }] | undefined { @@ -56,6 +58,18 @@ describe("msteams thread parent context injection", () => { return calls.find(([text]) => text.startsWith("Replying to @")); } + async function dispatchThreadReply(handler: MessageHandler, id: string) { + await handler({ + activity: buildChannelActivity({ id, replyToId: "thread-root-123" }), + sendActivity: vi.fn(async () => undefined), + } as unknown as Parameters[0]); + } + + async function dispatchTwoThreadReplies(handler: MessageHandler) { + await dispatchThreadReply(handler, "msg-reply-1"); + await dispatchThreadReply(handler, "msg-reply-2"); + } + beforeEach(() => { _resetThreadParentContextCachesForTest(); fetchChannelMessageMock.mockReset(); @@ -100,15 +114,7 @@ describe("msteams thread parent context injection", () => { const { deps } = createMessageHandlerDeps(cfg); const handler = createMSTeamsMessageHandler(deps); - await handler({ - activity: buildChannelActivity({ id: "msg-reply-1", replyToId: "thread-root-123" }), - sendActivity: vi.fn(async () => undefined), - } as unknown as Parameters[0]); - - await handler({ - activity: buildChannelActivity({ id: "msg-reply-2", replyToId: "thread-root-123" }), - sendActivity: vi.fn(async () => undefined), - } as unknown as Parameters[0]); + await dispatchTwoThreadReplies(handler); // Parent message fetched exactly once across two replies thanks to LRU cache. expect(fetchChannelMessageMock).toHaveBeenCalledTimes(1); @@ -123,15 +129,7 @@ describe("msteams thread parent context injection", () => { const { deps, enqueueSystemEvent } = createMessageHandlerDeps(cfg); const handler = createMSTeamsMessageHandler(deps); - await handler({ - activity: buildChannelActivity({ id: "msg-reply-1", replyToId: "thread-root-123" }), - sendActivity: vi.fn(async () => undefined), - } as unknown as Parameters[0]); - - await handler({ - activity: buildChannelActivity({ id: "msg-reply-2", replyToId: "thread-root-123" }), - sendActivity: vi.fn(async () => undefined), - } as unknown as Parameters[0]); + await dispatchTwoThreadReplies(handler); const parentCalls = enqueueSystemEvent.mock.calls.filter( ([text]) => typeof text === "string" && text.startsWith("Replying to @"),