






















@@ -10,10 +10,11 @@ import {
1010} from "openclaw/plugin-sdk/conversation-runtime";
1111import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
1212import { resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
13-import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
13+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
1414import type { ResolvedSlackAccount } from "../../accounts.js";
1515import type { SlackMessageEvent } from "../../types.js";
1616import type { SlackMonitorContext } from "../context.js";
17+import { resetSlackThreadStarterCacheForTest } from "../media.js";
1718import { resolveSlackMessageContent } from "./prepare-content.js";
1819import { prepareSlackMessage } from "./prepare.js";
1920import {
@@ -29,6 +30,10 @@ describe("slack prepareSlackMessage inbound contract", () => {
2930storeFixture.setup();
3031});
313233+beforeEach(() => {
34+resetSlackThreadStarterCacheForTest();
35+});
36+3237afterAll(() => {
3338storeFixture.cleanup();
3439});
@@ -126,6 +131,102 @@ describe("slack prepareSlackMessage inbound contract", () => {
126131return prepareMessageWith(ctx, createThreadAccount(), createThreadReplyMessage(overrides));
127132}
128133134+type ThreadContextAllowlistCaseParams = {
135+channel: string;
136+channelType: SlackMessageEvent["channel_type"];
137+user: string;
138+userName: string;
139+starterText: string;
140+followUpText: string;
141+startTs: string;
142+replyTs: string;
143+followUpTs: string;
144+currentTs: string;
145+channelsConfig?: Parameters<typeof createInboundSlackCtx>[0]["channelsConfig"];
146+resolveChannelName?: (channelId: string) => Promise<{
147+name?: string;
148+type?: SlackMessageEvent["channel_type"];
149+topic?: string;
150+purpose?: string;
151+}>;
152+};
153+154+async function prepareThreadContextAllowlistCase(params: ThreadContextAllowlistCaseParams) {
155+const { storePath } = storeFixture.makeTmpStorePath();
156+const replies = vi
157+.fn()
158+.mockResolvedValueOnce({
159+messages: [{ text: params.starterText, user: params.user, ts: params.startTs }],
160+})
161+.mockResolvedValueOnce({
162+messages: [
163+{ text: params.starterText, user: params.user, ts: params.startTs },
164+{ text: "assistant reply", bot_id: "B1", ts: params.replyTs },
165+{ text: params.followUpText, user: params.user, ts: params.followUpTs },
166+{ text: "current message", user: params.user, ts: params.currentTs },
167+],
168+response_metadata: { next_cursor: "" },
169+});
170+const ctx = createInboundSlackCtx({
171+cfg: {
172+session: { store: storePath },
173+channels: {
174+slack: {
175+enabled: true,
176+replyToMode: "all",
177+groupPolicy: "open",
178+contextVisibility: "allowlist",
179+},
180+},
181+} as OpenClawConfig,
182+appClient: { conversations: { replies } } as unknown as App["client"],
183+defaultRequireMention: false,
184+replyToMode: "all",
185+channelsConfig: params.channelsConfig,
186+});
187+ctx.allowFrom = ["u-owner"];
188+ctx.resolveUserName = async (id: string) => ({
189+name: id === params.user ? params.userName : "Owner",
190+});
191+if (params.resolveChannelName) {
192+ctx.resolveChannelName = params.resolveChannelName;
193+}
194+195+const prepared = await prepareSlackMessage({
196+ ctx,
197+account: createSlackAccount({
198+replyToMode: "all",
199+thread: { initialHistoryLimit: 20 },
200+}),
201+message: {
202+channel: params.channel,
203+channel_type: params.channelType,
204+user: params.user,
205+text: "current message",
206+ts: params.currentTs,
207+thread_ts: params.startTs,
208+} as SlackMessageEvent,
209+opts: { source: "message" },
210+});
211+212+return { prepared, replies };
213+}
214+215+function expectThreadContextAllowsHumanHistory(
216+prepared: Awaited<ReturnType<typeof prepareSlackMessage>>,
217+replies: ReturnType<typeof vi.fn>,
218+starterText: string,
219+followUpText: string,
220+) {
221+expect(prepared).toBeTruthy();
222+expect(prepared!.ctxPayload.ThreadStarterBody).toBe(starterText);
223+expect(prepared!.ctxPayload.ThreadHistoryBody).toContain(starterText);
224+expect(prepared!.ctxPayload.ThreadHistoryBody).toContain(followUpText);
225+expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("assistant reply");
226+expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message");
227+expect(replies).toHaveBeenCalledTimes(2);
228+}
229+129230function createDmScopeMainSlackCtx(): SlackMonitorContext {
130231const slackCtx = createInboundSlackCtx({
131232cfg: {
@@ -502,6 +603,102 @@ describe("slack prepareSlackMessage inbound contract", () => {
502603expect(replies).toHaveBeenCalledTimes(2);
503604});
504605606+it("uses room users allowlist for thread context filtering", async () => {
607+const { prepared, replies } = await prepareThreadContextAllowlistCase({
608+channel: "C123",
609+channelType: "channel",
610+user: "U1",
611+userName: "Alice",
612+starterText: "starter from room user",
613+followUpText: "allowed follow-up",
614+startTs: "100.000",
615+replyTs: "100.500",
616+followUpTs: "100.800",
617+currentTs: "101.000",
618+channelsConfig: {
619+C123: {
620+users: ["U1"],
621+requireMention: false,
622+},
623+},
624+resolveChannelName: async () => ({ name: "general", type: "channel" }),
625+});
626+627+expectThreadContextAllowsHumanHistory(
628+prepared,
629+replies,
630+"starter from room user",
631+"allowed follow-up",
632+);
633+});
634+635+it("does not apply the owner allowlist to open-room thread context", async () => {
636+const { prepared, replies } = await prepareThreadContextAllowlistCase({
637+channel: "C124",
638+channelType: "channel",
639+user: "U2",
640+userName: "Bob",
641+starterText: "starter from open room",
642+followUpText: "open-room follow-up",
643+startTs: "200.000",
644+replyTs: "200.500",
645+followUpTs: "200.800",
646+currentTs: "201.000",
647+channelsConfig: {
648+C124: {
649+requireMention: false,
650+},
651+},
652+resolveChannelName: async () => ({ name: "general", type: "channel" }),
653+});
654+655+expectThreadContextAllowsHumanHistory(
656+prepared,
657+replies,
658+"starter from open room",
659+"open-room follow-up",
660+);
661+});
662+663+it("does not apply the owner allowlist to open DMs when dmPolicy is open", async () => {
664+const { prepared, replies } = await prepareThreadContextAllowlistCase({
665+channel: "D300",
666+channelType: "im",
667+user: "U3",
668+userName: "Dana",
669+starterText: "starter from open dm",
670+followUpText: "dm follow-up",
671+startTs: "300.000",
672+replyTs: "300.500",
673+followUpTs: "300.800",
674+currentTs: "301.000",
675+});
676+677+expectThreadContextAllowsHumanHistory(
678+prepared,
679+replies,
680+"starter from open dm",
681+"dm follow-up",
682+);
683+});
684+685+it("does not apply the owner allowlist to MPIM thread context", async () => {
686+const { prepared, replies } = await prepareThreadContextAllowlistCase({
687+channel: "G400",
688+channelType: "mpim",
689+user: "U4",
690+userName: "Evan",
691+starterText: "starter from mpim",
692+followUpText: "mpim follow-up",
693+startTs: "400.000",
694+replyTs: "400.500",
695+followUpTs: "400.800",
696+currentTs: "401.000",
697+});
698+699+expectThreadContextAllowsHumanHistory(prepared, replies, "starter from mpim", "mpim follow-up");
700+});
701+505702it("skips loading thread history when thread session already exists in store (bloat fix)", async () => {
506703const { storePath } = storeFixture.makeTmpStorePath();
507704const cfg = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。