






















@@ -0,0 +1,152 @@
1+/**
2+ * Regression test for issue #69546.
3+ *
4+ * The access stage must resolve the agent route against whatever
5+ * `cfg` is passed in on each call, not against a snapshot captured
6+ * once. This test simulates a binding update between two consecutive
7+ * inbound events and asserts the second route reflects the new
8+ * `bindings[]`.
9+ */
10+11+import { describe, expect, it, vi } from "vitest";
12+import type { InboundPipelineDeps } from "../inbound-context.js";
13+import type { QueuedMessage } from "../message-queue.js";
14+import type { GatewayAccount, GatewayPluginRuntime } from "../types.js";
15+import { runAccessStage } from "./access-stage.js";
16+17+interface StubBinding {
18+match: { channel: string; accountPattern: string; peer?: string };
19+agentId: string;
20+}
21+22+interface StubCfg {
23+bindings: StubBinding[];
24+}
25+26+function buildAccount(overrides: Partial<GatewayAccount["config"]> = {}): GatewayAccount {
27+return {
28+accountId: "study",
29+appId: "1000000",
30+clientSecret: "secret",
31+markdownSupport: false,
32+config: {
33+dmPolicy: "open",
34+groupPolicy: "open",
35+ ...overrides,
36+},
37+};
38+}
39+40+function buildEvent(senderId: string): QueuedMessage {
41+return {
42+type: "c2c",
43+ senderId,
44+content: "hi",
45+messageId: `m-${senderId}`,
46+timestamp: "0",
47+};
48+}
49+50+function buildRuntime(
51+resolve: GatewayPluginRuntime["channel"]["routing"]["resolveAgentRoute"],
52+): GatewayPluginRuntime {
53+return {
54+channel: {
55+activity: { record: vi.fn() },
56+routing: { resolveAgentRoute: resolve },
57+reply: {
58+dispatchReplyWithBufferedBlockDispatcher: vi.fn(),
59+resolveEffectiveMessagesConfig: vi.fn(() => ({})),
60+finalizeInboundContext: vi.fn(),
61+formatInboundEnvelope: vi.fn(() => ""),
62+resolveEnvelopeFormatOptions: vi.fn(() => ({})),
63+},
64+text: { chunkMarkdownText: vi.fn(() => []) },
65+},
66+tts: { textToSpeech: vi.fn() },
67+};
68+}
69+70+function buildDeps(
71+cfg: unknown,
72+runtime: GatewayPluginRuntime,
73+account: GatewayAccount,
74+): InboundPipelineDeps {
75+return {
76+ account,
77+ cfg,
78+ runtime,
79+startTyping: vi.fn(),
80+adapters: {} as InboundPipelineDeps["adapters"],
81+};
82+}
83+84+describe("runAccessStage — dynamic cfg routing (#69546)", () => {
85+it("re-evaluates resolveAgentRoute against the cfg supplied on each call", () => {
86+const account = buildAccount();
87+const peerId = "480562E9913A985D4A79822A643E27B6";
88+89+const accountOnly: StubCfg = {
90+bindings: [{ match: { channel: "qqbot", accountPattern: "study" }, agentId: "study" }],
91+};
92+const withPeer: StubCfg = {
93+bindings: [
94+{
95+match: { channel: "qqbot", accountPattern: "study", peer: `direct:${peerId}` },
96+agentId: "tutor",
97+},
98+{ match: { channel: "qqbot", accountPattern: "study" }, agentId: "study" },
99+],
100+};
101+102+const captured: Array<{ cfg: unknown; peerId: string }> = [];
103+const runtime = buildRuntime((params) => {
104+const cfg = params.cfg as StubCfg;
105+captured.push({ cfg, peerId: params.peer.id });
106+const exact = cfg.bindings.find((b) => b.match.peer === `direct:${params.peer.id}`);
107+const fallback = cfg.bindings.find((b) => !b.match.peer);
108+const agent = exact?.agentId ?? fallback?.agentId;
109+return { sessionKey: `qqbot:${params.peer.id}`, accountId: params.accountId, agentId: agent };
110+});
111+112+const event = buildEvent(peerId);
113+114+const first = runAccessStage(event, buildDeps(accountOnly, runtime, account));
115+expect(first.kind).toBe("allow");
116+if (first.kind === "allow") {
117+expect(first.route.agentId).toBe("study");
118+}
119+120+const second = runAccessStage(event, buildDeps(withPeer, runtime, account));
121+expect(second.kind).toBe("allow");
122+if (second.kind === "allow") {
123+expect(second.route.agentId).toBe("tutor");
124+}
125+126+expect(captured).toHaveLength(2);
127+expect(captured[0]?.cfg).toBe(accountOnly);
128+expect(captured[1]?.cfg).toBe(withPeer);
129+});
130+131+it("never reads bindings from a previous cfg reference", () => {
132+const account = buildAccount();
133+const seenCfgs = new Set<unknown>();
134+const runtime = buildRuntime((params) => {
135+seenCfgs.add(params.cfg);
136+return { sessionKey: `s:${params.peer.id}`, accountId: params.accountId };
137+});
138+139+const cfgA: StubCfg = { bindings: [] };
140+const cfgB: StubCfg = { bindings: [] };
141+const cfgC: StubCfg = { bindings: [] };
142+143+runAccessStage(buildEvent("a"), buildDeps(cfgA, runtime, account));
144+runAccessStage(buildEvent("b"), buildDeps(cfgB, runtime, account));
145+runAccessStage(buildEvent("c"), buildDeps(cfgC, runtime, account));
146+147+expect(seenCfgs.size).toBe(3);
148+expect(seenCfgs.has(cfgA)).toBe(true);
149+expect(seenCfgs.has(cfgB)).toBe(true);
150+expect(seenCfgs.has(cfgC)).toBe(true);
151+});
152+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。