























1+// Session origin must drop the prior channel's identity when a dmScope:"main" session moves
2+// across providers, so channel-keyed fields do not reference a now-inactive channel.
3+import { describe, expect, it } from "vitest";
4+import type { MsgContext } from "../../auto-reply/templating.js";
5+import { buildChannelInboundEventContext } from "../../channels/inbound-event/context.js";
6+import { deriveSessionMetaPatch } from "./metadata.js";
7+import type { SessionEntry } from "./types.js";
8+9+const sessionKey = "agent:user";
10+11+function applyOrigin(existing: SessionEntry | undefined, ctx: Partial<MsgContext>): SessionEntry {
12+const patch = deriveSessionMetaPatch({
13+ctx: ctx as MsgContext,
14+ sessionKey,
15+ existing,
16+});
17+return { ...existing, ...patch } as SessionEntry;
18+}
19+20+const slackTurn = {
21+Provider: "slack",
22+Surface: "slack",
23+ChatType: "direct",
24+From: "slack:U0001",
25+To: "slack:D111SLACK",
26+NativeChannelId: "D111SLACK",
27+NativeDirectUserId: "U0001",
28+AccountId: "slack-team-1",
29+MessageThreadId: "1700000000.000100",
30+} satisfies Partial<MsgContext>;
31+32+const telegramTurn = {
33+Provider: "telegram",
34+Surface: "telegram",
35+ChatType: "direct",
36+From: "telegram:42",
37+To: "telegram:42",
38+AccountId: "telegram-bot-1",
39+} satisfies Partial<MsgContext>;
40+41+describe("session origin across a channel switch", () => {
42+it("clears stale channel-keyed fields when provider changes and the new turn omits them", () => {
43+const afterSlack = applyOrigin(undefined, slackTurn);
44+expect(afterSlack.origin?.nativeChannelId).toBe("D111SLACK");
45+expect(afterSlack.origin?.threadId).toBe("1700000000.000100");
46+47+const afterTelegram = applyOrigin(afterSlack, telegramTurn);
48+49+// Provider/surface flip to Telegram, and the Slack-only identity must not survive.
50+expect(afterTelegram.origin?.provider).toBe("telegram");
51+expect(afterTelegram.origin?.surface).toBe("telegram");
52+expect(afterTelegram.origin?.accountId).toBe("telegram-bot-1");
53+expect(afterTelegram.origin?.nativeChannelId).toBeUndefined();
54+expect(afterTelegram.origin?.nativeDirectUserId).toBeUndefined();
55+expect(afterTelegram.origin?.threadId).toBeUndefined();
56+});
57+58+it("does not re-stamp the prior channel id on subsequent same-channel turns", () => {
59+const afterSlack = applyOrigin(undefined, slackTurn);
60+const afterTelegram = applyOrigin(afterSlack, telegramTurn);
61+const afterTelegramAgain = applyOrigin(afterTelegram, telegramTurn);
62+63+expect(afterTelegramAgain.origin?.provider).toBe("telegram");
64+expect(afterTelegramAgain.origin?.nativeChannelId).toBeUndefined();
65+expect(afterTelegramAgain.origin?.threadId).toBeUndefined();
66+});
67+68+it("clears stale account id when provider changes and the new turn omits it", () => {
69+const afterSlack = applyOrigin(undefined, slackTurn);
70+const telegramWithoutAccount = {
71+Provider: "telegram",
72+Surface: "telegram",
73+ChatType: "direct",
74+From: "telegram:42",
75+To: "telegram:42",
76+} satisfies Partial<MsgContext>;
77+78+const afterTelegram = applyOrigin(afterSlack, telegramWithoutAccount);
79+80+expect(afterTelegram.origin?.provider).toBe("telegram");
81+expect(afterTelegram.origin?.accountId).toBeUndefined();
82+});
83+84+it("adopts the new channel's identity when the new turn supplies it", () => {
85+const afterSlack = applyOrigin(undefined, slackTurn);
86+const telegramWithChannel = {
87+ ...telegramTurn,
88+NativeChannelId: "C222TG",
89+MessageThreadId: 555,
90+} satisfies Partial<MsgContext>;
91+92+const afterTelegram = applyOrigin(afterSlack, telegramWithChannel);
93+expect(afterTelegram.origin?.nativeChannelId).toBe("C222TG");
94+expect(afterTelegram.origin?.threadId).toBe(555);
95+});
96+97+it("preserves sparse channel metadata across turns on the same provider", () => {
98+const afterSlack = applyOrigin(undefined, slackTurn);
99+const slackFollowUp = {
100+Provider: "slack",
101+Surface: "slack",
102+ChatType: "direct",
103+From: "slack:U0001",
104+To: "slack:D111SLACK",
105+AccountId: "slack-team-1",
106+} satisfies Partial<MsgContext>;
107+108+const afterFollowUp = applyOrigin(afterSlack, slackFollowUp);
109+// Same provider: the established channel id and thread are retained when omitted.
110+expect(afterFollowUp.origin?.nativeChannelId).toBe("D111SLACK");
111+expect(afterFollowUp.origin?.threadId).toBe("1700000000.000100");
112+});
113+114+it("clears stale channel-keyed fields when the account changes and the new turn omits them", () => {
115+const afterSlack = applyOrigin(undefined, slackTurn);
116+const slackOtherAccount = {
117+Provider: "slack",
118+Surface: "slack",
119+ChatType: "direct",
120+From: "slack:U0002",
121+To: "slack:D222SLACK",
122+AccountId: "slack-team-2",
123+} satisfies Partial<MsgContext>;
124+125+const afterAccountSwitch = applyOrigin(afterSlack, slackOtherAccount);
126+127+expect(afterAccountSwitch.origin?.provider).toBe("slack");
128+expect(afterAccountSwitch.origin?.accountId).toBe("slack-team-2");
129+expect(afterAccountSwitch.origin?.nativeChannelId).toBeUndefined();
130+expect(afterAccountSwitch.origin?.nativeDirectUserId).toBeUndefined();
131+expect(afterAccountSwitch.origin?.threadId).toBeUndefined();
132+});
133+134+it("preserves sparse existing channel metadata when optional identity fields are first populated", () => {
135+const existing = {
136+sessionId: "session-1",
137+updatedAt: 1,
138+origin: {
139+provider: "slack",
140+nativeChannelId: "D111SLACK",
141+threadId: "1700000000.000100",
142+},
143+} satisfies SessionEntry;
144+const slackFollowUp = {
145+Provider: "slack",
146+Surface: "slack",
147+ChatType: "direct",
148+From: "slack:U0001",
149+To: "slack:D111SLACK",
150+AccountId: "slack-team-1",
151+} satisfies Partial<MsgContext>;
152+153+const afterFollowUp = applyOrigin(existing, slackFollowUp);
154+155+expect(afterFollowUp.origin?.surface).toBe("slack");
156+expect(afterFollowUp.origin?.accountId).toBe("slack-team-1");
157+expect(afterFollowUp.origin?.nativeChannelId).toBe("D111SLACK");
158+expect(afterFollowUp.origin?.threadId).toBe("1700000000.000100");
159+});
160+});
161+162+// Drive the production inbound-event context builder so the bug premise itself is proven, not
163+// assumed: a Slack DM turn populates ctx.NativeChannelId (from conversation.nativeChannelId), a
164+// Telegram DM turn omits it, and the same derivation path runs that real context.
165+function buildDirectTurn(opts: {
166+provider: string;
167+from: string;
168+to: string;
169+accountId: string;
170+conversationId: string;
171+nativeChannelId?: string;
172+}): MsgContext {
173+return buildChannelInboundEventContext({
174+channel: opts.provider,
175+provider: opts.provider,
176+surface: opts.provider,
177+accountId: opts.accountId,
178+messageId: "m-1",
179+from: opts.from,
180+sender: { id: opts.from },
181+conversation: {
182+kind: "direct",
183+id: opts.conversationId,
184+ ...(opts.nativeChannelId ? { nativeChannelId: opts.nativeChannelId } : {}),
185+},
186+route: { agentId: "main", accountId: opts.accountId, routeSessionKey: sessionKey },
187+reply: { to: opts.to },
188+message: { rawBody: "hi" },
189+}) as MsgContext;
190+}
191+192+describe("session origin across a channel switch (real inbound-event context builder)", () => {
193+const slackCtx = buildDirectTurn({
194+provider: "slack",
195+from: "slack:U0001",
196+to: "slack:D111SLACK",
197+accountId: "slack-team-1",
198+conversationId: "D111SLACK",
199+nativeChannelId: "D111SLACK",
200+});
201+const telegramCtx = buildDirectTurn({
202+provider: "telegram",
203+from: "telegram:42",
204+to: "telegram:42",
205+accountId: "telegram-bot-1",
206+conversationId: "42",
207+});
208+209+it("confirms the premise: Slack DM context supplies NativeChannelId, Telegram DM context omits it", () => {
210+expect(slackCtx.NativeChannelId).toBe("D111SLACK");
211+expect(telegramCtx.NativeChannelId).toBeUndefined();
212+});
213+214+it("resets the stale Slack channel id after a real-context Slack->Telegram switch", () => {
215+const afterSlack = applyOrigin(undefined, slackCtx);
216+expect(afterSlack.origin?.nativeChannelId).toBe("D111SLACK");
217+218+const afterTelegram = applyOrigin(afterSlack, telegramCtx);
219+expect(afterTelegram.origin?.provider).toBe("telegram");
220+expect(afterTelegram.origin?.nativeChannelId).toBeUndefined();
221+});
222+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。