Files
openclaw/extensions/msteams/src/sdk-proactive.test.ts
Peter Steinberger a6b77ffc07 fix(msteams): preserve replies after durable ingress replay (#126169)
* fix(msteams): preserve replies across ingress replay

Recovered Teams channel and group-chat responses now preserve reply and quote context across durable ingress replay. Discovery metadata also advertises the existing group and reaction capabilities.

* chore(msteams): document replay assertion safety

* test(msteams): normalize replay delivery errors
2026-08-18 22:22:13 -07:00

126 lines
3.9 KiB
TypeScript

// Msteams tests cover sdk proactive plugin behavior.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { sendMSTeamsActivityWithReference } from "./sdk-proactive.js";
import type { MSTeamsApp } from "./sdk.js";
const clientState = vi.hoisted(() => ({
created: [] as Array<{ serviceUrl: string; http: unknown }>,
create: vi.fn(async (_payload: { conversationId: string; activity: unknown }) => ({
id: "activity-1",
})),
}));
vi.mock("@microsoft/teams.api", async (importOriginal) => ({
...(await importOriginal<typeof import("@microsoft/teams.api")>()),
Client: vi.fn(function MockClient(this: unknown, serviceUrl: string, http: unknown) {
clientState.created.push({ serviceUrl, http });
return {
serviceUrl,
conversations: {
activities: (conversationId: string) => ({
create: (activity: unknown) =>
clientState.create({
conversationId,
activity,
}),
}),
},
};
}),
}));
describe("sendMSTeamsActivityWithReference", () => {
beforeEach(() => {
clientState.created.length = 0;
clientState.create.mockClear().mockResolvedValue({ id: "activity-1" });
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("sends a legacy bot-only imported reference instead of rejecting it", async () => {
vi.stubEnv("SERVICE_URL", "https://bot.example.com/api/messages");
const app = {
client: { request: vi.fn() },
api: { serviceUrl: "https://smba.trafficmanager.net/teams" },
} as unknown as MSTeamsApp;
const result = await sendMSTeamsActivityWithReference(
app,
{
serviceUrl: "https://smba.trafficmanager.net/amer/",
bot: { id: "28:legacy-bot", name: "OpenClaw" },
user: { id: "29:user" },
conversation: {
id: "19:conversation@thread.tacv2",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
},
{ type: "message", text: "hello" },
);
expect(result).toMatchObject({ id: "activity-1" });
expect(clientState.create).toHaveBeenCalledTimes(1);
});
it("sends through a reference-scoped API client without the protected SDK activitySender", async () => {
vi.stubEnv("SERVICE_URL", "https://bot.example.com/api/messages");
const httpClient = { request: vi.fn() };
const app = {
client: httpClient,
api: {
serviceUrl: "https://smba.trafficmanager.net/teams",
conversations: {
activities: () => ({
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
}),
},
},
} as unknown as MSTeamsApp;
const result = await sendMSTeamsActivityWithReference(
app,
{
serviceUrl: "https://smba.trafficmanager.net/amer/",
agent: { id: "28:bot", name: "OpenClaw", role: "bot" },
user: { id: "29:user", aadObjectId: "aad-user" },
conversation: {
id: "19:conversation@thread.tacv2",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
},
{ type: "message", text: "hello" },
{ serviceUrlBoundary: { cloud: "Public" } },
);
expect(result).toMatchObject({ id: "activity-1" });
expect(clientState.created).toEqual([
{
serviceUrl: "https://smba.trafficmanager.net/amer",
http: httpClient,
},
]);
expect(clientState.create).toHaveBeenCalledWith({
conversationId: "19:conversation@thread.tacv2",
activity: expect.objectContaining({
type: "message",
text: "hello",
from: { id: "28:bot", name: "OpenClaw", role: "bot" },
conversation: {
id: "19:conversation@thread.tacv2",
conversationType: "personal",
tenantId: "tenant-1",
},
channelData: { tenant: { id: "tenant-1" } },
}),
});
});
});