fix(msteams): persist conversation reference during DM pairing (#60432)

* fix(msteams): persist conversation reference during DM pairing (#43323)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
This commit is contained in:
Brad Groux 2026-04-04 02:38:54 -05:00 committed by GitHub
parent 06c6ff6670
commit dd2faa3764
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 27 deletions

View File

@ -235,7 +235,7 @@ describe("msteams monitor handler authz", () => {
});
it("keeps the DM pairing path wired through shared access resolution", async () => {
const { deps, upsertPairingRequest } = createDeps({
const { conversationStore, deps, upsertPairingRequest, recordInboundSession } = createDeps({
channels: {
msteams: {
dmPolicy: "pairing",
@ -262,8 +262,18 @@ describe("msteams monitor handler authz", () => {
conversation: {
id: "a:personal-chat",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
serviceUrl: "https://smba.trafficmanager.net/amer/",
locale: "en-US",
channelData: {},
entities: [
{
type: "clientInfo",
timezone: "America/New_York",
},
],
attachments: [],
},
sendActivity: vi.fn(async () => undefined),
@ -275,6 +285,33 @@ describe("msteams monitor handler authz", () => {
id: "new-user-aad",
meta: { name: "New User" },
});
expect(conversationStore.upsert).toHaveBeenCalledWith("a:personal-chat", {
activityId: "msg-pairing",
user: {
id: "new-user-id",
aadObjectId: "new-user-aad",
name: "New User",
},
agent: {
id: "bot-id",
name: "Bot",
},
bot: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "a:personal-chat",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
serviceUrl: "https://smba.trafficmanager.net/amer/",
locale: "en-US",
timezone: "America/New_York",
});
expect(recordInboundSession).not.toHaveBeenCalled();
expect(runtimeApiMockState.dispatchReplyFromConfigWithSettledDispatcher).not.toHaveBeenCalled();
});
it("logs an info drop reason when dmPolicy allowlist rejects a sender", async () => {

View File

@ -52,6 +52,37 @@ import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message
import { resolveMSTeamsSenderAccess } from "./access.js";
import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
function buildStoredConversationReference(params: {
activity: MSTeamsTurnContext["activity"];
conversationId: string;
conversationType: string;
teamId?: string;
}): StoredConversationReference {
const { activity, conversationId, conversationType, teamId } = params;
const from = activity.from;
const conversation = activity.conversation;
const agent = activity.recipient;
const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
| { timezone?: string }
| undefined;
return {
activityId: activity.id,
user: from ? { id: from.id, name: from.name, aadObjectId: from.aadObjectId } : undefined,
agent,
bot: agent ? { id: agent.id, name: agent.name } : undefined,
conversation: {
id: conversationId,
conversationType,
tenantId: conversation?.tenantId,
},
teamId,
channelId: activity.channelId,
serviceUrl: activity.serviceUrl,
locale: activity.locale,
...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}),
};
}
export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const {
cfg,
@ -140,6 +171,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId);
const conversationType = conversation?.conversationType ?? "personal";
const teamId = activity.channelData?.team?.id;
const conversationRef = buildStoredConversationReference({
activity,
conversationId,
conversationType,
teamId,
});
const {
dmPolicy,
@ -177,6 +214,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
allowNameMatching,
});
if (access.decision === "pairing") {
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
log.debug?.("failed to save conversation reference", {
error: formatUnknownError(err),
});
});
const request = await pairing.upsertPairingRequest({
id: senderId,
meta: { name: senderName },
@ -306,30 +348,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
return;
}
// Extract clientInfo entity (Teams sends this on every activity with timezone, locale, etc.)
const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
| { timezone?: string; locale?: string; country?: string; platform?: string }
| undefined;
// Build conversation reference for proactive replies.
const agent = activity.recipient;
const conversationRef: StoredConversationReference = {
activityId: activity.id,
user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId },
agent,
bot: agent ? { id: agent.id, name: agent.name } : undefined,
conversation: {
id: conversationId,
conversationType,
tenantId: conversation?.tenantId,
},
teamId,
channelId: activity.channelId,
serviceUrl: activity.serviceUrl,
locale: activity.locale,
// Only set timezone if present (preserve previously stored value on next upsert)
...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}),
};
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
log.debug?.("failed to save conversation reference", {
error: formatUnknownError(err),
@ -642,8 +660,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
// Use Teams clientInfo timezone if no explicit userTimezone is configured.
// This ensures the agent knows the sender's timezone for time-aware responses
// and proactive sends within the same session.
// Apply Teams clientInfo timezone if no explicit userTimezone is configured.
const senderTimezone = clientInfo?.timezone || conversationRef.timezone;
const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
| { timezone?: string }
| undefined;
const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone;
const configOverride =
senderTimezone && !cfg.agents?.defaults?.userTimezone
? {