From 5cb4bbcd41ef453402dcb15123c0a7a5cdbbd5dc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 24 Aug 2026 01:23:43 -0700 Subject: [PATCH] fix(msteams): use one Teams API runtime entrypoint (#128572) * fix(msteams): use one Teams API runtime entrypoint * test(msteams): exercise packaged CJS loader * test(msteams): run packaged build through CLI * chore(msteams): document SDK assertion safety --- .../msteams/src/sdk-proactive.process.test.ts | 165 ++++++++++++++++++ extensions/msteams/src/sdk-proactive.ts | 20 +-- 2 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 extensions/msteams/src/sdk-proactive.process.test.ts diff --git a/extensions/msteams/src/sdk-proactive.process.test.ts b/extensions/msteams/src/sdk-proactive.process.test.ts new file mode 100644 index 000000000000..c3306d577077 --- /dev/null +++ b/extensions/msteams/src/sdk-proactive.process.test.ts @@ -0,0 +1,165 @@ +// Microsoft Teams process coverage protects real SDK CommonJS import ordering. +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; + +const execFileAsync = promisify(execFile); + +describe("sendMSTeamsActivityWithReference SDK import ordering", () => { + it("keeps root exports intact when quoted behavior is the first SDK access", async () => { + await execFileAsync( + process.execPath, + ["scripts/lib/plugin-npm-runtime-build.mjs", "extensions/msteams"], + { cwd: process.cwd() }, + ); + const outDir = path.join(process.cwd(), "extensions/msteams/dist"); + const proactiveArtifact = fs + .readdirSync(outDir) + .filter((entry) => entry.endsWith(".cjs")) + .map((entry) => path.join(outDir, entry)) + .find((entry) => + fs + .readFileSync(entry, "utf8") + .includes('Object.defineProperty(exports, "sendMSTeamsActivityWithReference"'), + ); + if (!proactiveArtifact) { + throw new Error("Microsoft Teams CommonJS runtime omitted the proactive send helper"); + } + const proactiveArtifactSource = fs.readFileSync(proactiveArtifact, "utf8"); + expect(proactiveArtifactSource).toContain('import("@microsoft/teams.api")'); + expect(proactiveArtifactSource).not.toContain("@microsoft/teams.api/dist/"); + + const fixture = ` + import assert from "node:assert/strict"; + import { createRequire } from "node:module"; + + const require = createRequire(import.meta.url); + const Module = require("node:module"); + const originalLoad = Module._load; + const universalStub = new Proxy( + function universalStub() { + return universalStub; + }, + { + apply: () => universalStub, + construct: () => universalStub, + get: () => universalStub, + }, + ); + const hostSdkStub = new Proxy( + {}, + { + get: (_target, key) => { + if (key === "createLazyRuntimeModule") { + return (importer) => { + let pending; + return () => (pending ??= importer()); + }; + } + if (key === "getOrCreateGlobalSingleton") { + return (_key, create) => create(); + } + return universalStub; + }, + }, + ); + // The built plugin expects an installed OpenClaw host. Stub unrelated host SDK exports so + // this child isolates the emitted Teams loader and the real pinned Teams CommonJS package. + Module._load = function load(request, parent, isMain) { + if (request.startsWith("openclaw/plugin-sdk/")) { + return hostSdkStub; + } + return originalLoad.call(this, request, parent, isMain); + }; + + const { sendMSTeamsActivityWithReference } = + require(process.env.OPENCLAW_MSTEAMS_PROACTIVE_ARTIFACT); + const quotedCreates = []; + const posts = []; + const app = { + client: { + request: async () => ({}), + post: async (url, activity) => { + posts.push({ url, activity }); + return { data: { id: "normal-proactive" } }; + }, + }, + api: { + serviceUrl: "https://smba.trafficmanager.net/amer", + conversations: { + activities: () => ({ + create: async (activity) => { + quotedCreates.push(activity); + return { id: "quoted-replay" }; + }, + update: async () => ({}), + delete: async () => ({}), + }), + }, + }, + }; + const reference = { + agent: { id: "28:bot", role: "bot" }, + user: { id: "29:user" }, + conversation: { + id: "19:conversation@thread.v2", + conversationType: "groupChat", + }, + }; + + // The structural app selects the quote path without loading the full Teams app first. + await sendMSTeamsActivityWithReference( + app, + { ...reference, serviceUrl: "https://smba.trafficmanager.net/amer" }, + { type: "message", text: "Recovered reply" }, + { quoteActivityId: "incoming-activity" }, + ); + assert.equal( + quotedCreates[0].text, + ' Recovered reply', + ); + + const api = await import("@microsoft/teams.api"); + assert.equal(typeof api.Client, "function"); + assert.equal(typeof api.MessageActivityInput, "function"); + assert.equal(typeof api.toActivityParams, "function"); + assert.doesNotThrow(() => + api.toActivityParams({ type: "message", text: "Direct conversion" }), + ); + + await assert.doesNotReject(() => + sendMSTeamsActivityWithReference( + app, + { ...reference, serviceUrl: "https://smba.trafficmanager.net/teams" }, + { type: "message", text: "Normal send" }, + ), + ); + assert.equal(posts.length, 1); + assert.equal( + posts[0].url, + "https://smba.trafficmanager.net/teams/v3/conversations/19:conversation@thread.v2/activities", + ); + assert.equal(posts[0].activity.text, "Normal send"); + process.stdout.write("root-sdk-ok"); + `; + + const { stdout, stderr } = await execFileAsync( + process.execPath, + ["--import", "tsx", "--input-type=module", "--eval", fixture], + { + cwd: process.cwd(), + env: { + ...process.env, + NODE_DISABLE_COMPILE_CACHE: "1", + OPENCLAW_MSTEAMS_PROACTIVE_ARTIFACT: proactiveArtifact, + VITEST: undefined, + }, + }, + ); + + expect(stderr).toBe(""); + expect(stdout).toBe("root-sdk-ok"); + }, 30_000); +}); diff --git a/extensions/msteams/src/sdk-proactive.ts b/extensions/msteams/src/sdk-proactive.ts index 2363b5964546..86494c85233c 100644 --- a/extensions/msteams/src/sdk-proactive.ts +++ b/extensions/msteams/src/sdk-proactive.ts @@ -64,21 +64,21 @@ type MSTeamsProactiveOptions = { serviceUrlBoundary?: MSTeamsSdkCloudOptions; }; -const loadMSTeamsApiClient = createLazyRuntimeModule(() => - import("@microsoft/teams.api").then((api) => ({ Client: api.Client })), -); - -const loadMSTeamsQuoteModule = createLazyRuntimeModule(() => - import("@microsoft/teams.api/dist/activities/message/message.js").then((message) => ({ - MessageActivityInput: message.MessageActivityInput, - })), +const loadMSTeamsApiModule = createLazyRuntimeModule( + () => + // SAFETY: the 2.0.14 CJS root exports MessageActivityInput, while its declarations omit it. + import("@microsoft/teams.api") as unknown as Promise< + typeof import("@microsoft/teams.api") & { + MessageActivityInput: typeof import("@microsoft/teams.api/dist/activities/message/message.js").MessageActivityInput; + } + >, ); async function quoteMSTeamsActivity( activity: MSTeamsActivityLike, messageId: string, ): Promise { - const { MessageActivityInput } = await loadMSTeamsQuoteModule(); + const { MessageActivityInput } = await loadMSTeamsApiModule(); if (typeof activity === "string") { return new MessageActivityInput(activity).prependQuote(messageId); } @@ -203,7 +203,7 @@ async function getApiClientForReference( return api; } - const { Client } = await loadMSTeamsApiClient(); + const { Client } = await loadMSTeamsApiModule(); return new Client(ref.serviceUrl, httpClient) as unknown as MSTeamsApiClient; }