diff --git a/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts b/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts index eadcd9a2bbc..8aeeb2be822 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts @@ -1,36 +1,24 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import type { App } from "@slack/bolt"; import { resolveEnvelopeFormatOptions } from "openclaw/plugin-sdk/channel-inbound"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { SlackMessageEvent } from "../../types.js"; import { resolveSlackThreadContextData } from "./prepare-thread-context.js"; -import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js"; +import { + createInboundSlackTestContext, + createSlackSessionStoreFixture, + createSlackTestAccount, +} from "./prepare.test-helpers.js"; describe("resolveSlackThreadContextData", () => { - let fixtureRoot = ""; - let caseId = 0; - - function makeTmpStorePath() { - if (!fixtureRoot) { - throw new Error("fixtureRoot missing"); - } - const dir = path.join(fixtureRoot, `case-${caseId++}`); - fs.mkdirSync(dir); - return { dir, storePath: path.join(dir, "sessions.json") }; - } + const storeFixture = createSlackSessionStoreFixture("openclaw-slack-thread-context-"); beforeAll(() => { - fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-thread-context-")); + storeFixture.setup(); }); afterAll(() => { - if (fixtureRoot) { - fs.rmSync(fixtureRoot, { recursive: true, force: true }); - fixtureRoot = ""; - } + storeFixture.cleanup(); }); function createThreadContext(params: { replies: unknown }) { @@ -56,16 +44,15 @@ describe("resolveSlackThreadContextData", () => { } as SlackMessageEvent; } - it("omits non-allowlisted starter text and thread history messages", async () => { - const { storePath } = makeTmpStorePath(); + async function resolveAllowlistedThreadContext(params: { + repliesMessages: Array>; + threadStarter: { text: string; userId: string; ts: string }; + allowFromLower: string[]; + allowNameMatching: boolean; + }) { + const { storePath } = storeFixture.makeTmpStorePath(); const replies = vi.fn().mockResolvedValue({ - messages: [ - { text: "starter secret", user: "U2", ts: "100.000" }, - { text: "assistant reply", bot_id: "B1", ts: "100.500" }, - { text: "blocked follow-up", user: "U2", ts: "100.700" }, - { text: "allowed follow-up", user: "U1", ts: "100.800" }, - { text: "current message", user: "U1", ts: "101.000" }, - ], + messages: params.repliesMessages, response_metadata: { next_cursor: "" }, }); const ctx = createThreadContext({ replies }); @@ -79,19 +66,36 @@ describe("resolveSlackThreadContextData", () => { message: createThreadMessage(), isThreadReply: true, threadTs: "100.000", + threadStarter: params.threadStarter, + roomLabel: "#general", + storePath, + sessionKey: "thread-session", + allowFromLower: params.allowFromLower, + allowNameMatching: params.allowNameMatching, + contextVisibilityMode: "allowlist", + envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig), + effectiveDirectMedia: null, + }); + + return { replies, result }; + } + + it("omits non-allowlisted starter text and thread history messages", async () => { + const { replies, result } = await resolveAllowlistedThreadContext({ + repliesMessages: [ + { text: "starter secret", user: "U2", ts: "100.000" }, + { text: "assistant reply", bot_id: "B1", ts: "100.500" }, + { text: "blocked follow-up", user: "U2", ts: "100.700" }, + { text: "allowed follow-up", user: "U1", ts: "100.800" }, + { text: "current message", user: "U1", ts: "101.000" }, + ], threadStarter: { text: "starter secret", userId: "U2", ts: "100.000", }, - roomLabel: "#general", - storePath, - sessionKey: "thread-session", allowFromLower: ["u1"], allowNameMatching: false, - contextVisibilityMode: "allowlist", - envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig), - effectiveDirectMedia: null, }); expect(result.threadStarterBody).toBeUndefined(); @@ -105,39 +109,19 @@ describe("resolveSlackThreadContextData", () => { }); it("keeps starter text and history when allowNameMatching authorizes the sender", async () => { - const { storePath } = makeTmpStorePath(); - const replies = vi.fn().mockResolvedValue({ - messages: [ + const { result } = await resolveAllowlistedThreadContext({ + repliesMessages: [ { text: "starter from Alice", user: "U1", ts: "100.000" }, { text: "blocked follow-up", user: "U2", ts: "100.700" }, { text: "current message", user: "U1", ts: "101.000" }, ], - response_metadata: { next_cursor: "" }, - }); - const ctx = createThreadContext({ replies }); - ctx.resolveUserName = async (id: string) => ({ - name: id === "U1" ? "Alice" : "Mallory", - }); - - const result = await resolveSlackThreadContextData({ - ctx, - account: createSlackTestAccount({ thread: { initialHistoryLimit: 20 } }), - message: createThreadMessage(), - isThreadReply: true, - threadTs: "100.000", threadStarter: { text: "starter from Alice", userId: "U1", ts: "100.000", }, - roomLabel: "#general", - storePath, - sessionKey: "thread-session", allowFromLower: ["alice"], allowNameMatching: true, - contextVisibilityMode: "allowlist", - envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig), - effectiveDirectMedia: null, }); expect(result.threadStarterBody).toBe("starter from Alice"); diff --git a/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts b/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts index d1838d4ea69..4ea2c024aa6 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; @@ -70,3 +73,29 @@ export function createSlackTestAccount( dm: config.dm, }; } + +export function createSlackSessionStoreFixture(prefix: string) { + let fixtureRoot = ""; + let caseId = 0; + + return { + setup() { + fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + }, + cleanup() { + if (!fixtureRoot) { + return; + } + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + fixtureRoot = ""; + }, + makeTmpStorePath() { + if (!fixtureRoot) { + throw new Error("fixtureRoot missing"); + } + const dir = path.join(fixtureRoot, `case-${caseId++}`); + fs.mkdirSync(dir); + return { dir, storePath: path.join(dir, "sessions.json") }; + }, + }; +} diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 81988c83bd9..20ac6e4cf8a 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -1,6 +1,4 @@ import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import type { App } from "@slack/bolt"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { resolveAgentRoute } from "openclaw/plugin-sdk/routing"; @@ -11,30 +9,21 @@ import type { ResolvedSlackAccount } from "../../accounts.js"; import type { SlackMessageEvent } from "../../types.js"; import type { SlackMonitorContext } from "../context.js"; import { prepareSlackMessage } from "./prepare.js"; -import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js"; +import { + createInboundSlackTestContext, + createSlackSessionStoreFixture, + createSlackTestAccount, +} from "./prepare.test-helpers.js"; describe("slack prepareSlackMessage inbound contract", () => { - let fixtureRoot = ""; - let caseId = 0; - - function makeTmpStorePath() { - if (!fixtureRoot) { - throw new Error("fixtureRoot missing"); - } - const dir = path.join(fixtureRoot, `case-${caseId++}`); - fs.mkdirSync(dir); - return { dir, storePath: path.join(dir, "sessions.json") }; - } + const storeFixture = createSlackSessionStoreFixture("openclaw-slack-thread-"); beforeAll(() => { - fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-thread-")); + storeFixture.setup(); }); afterAll(() => { - if (fixtureRoot) { - fs.rmSync(fixtureRoot, { recursive: true, force: true }); - fixtureRoot = ""; - } + storeFixture.cleanup(); }); const createInboundSlackCtx = createInboundSlackTestContext; @@ -449,7 +438,7 @@ describe("slack prepareSlackMessage inbound contract", () => { }); it("marks first thread turn and injects thread history for a new thread session", async () => { - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); const replies = vi .fn() .mockResolvedValueOnce({ @@ -490,7 +479,7 @@ describe("slack prepareSlackMessage inbound contract", () => { }); it("skips loading thread history when thread session already exists in store (bloat fix)", async () => { - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); const cfg = { session: { store: storePath }, channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } }, @@ -578,7 +567,7 @@ describe("slack prepareSlackMessage inbound contract", () => { }); it("creates thread session for top-level DM when replyToMode=all", async () => { - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); const slackCtx = createInboundSlackCtx({ cfg: { session: { store: storePath }, @@ -713,27 +702,14 @@ describe("prepareSlackMessage sender prefix", () => { }); describe("slack thread.requireExplicitMention", () => { - let fixtureRoot = ""; - let caseId = 0; - - function makeTmpStorePath() { - if (!fixtureRoot) { - throw new Error("fixtureRoot missing"); - } - const dir = path.join(fixtureRoot, `require-explicit-${caseId++}`); - fs.mkdirSync(dir); - return { dir, storePath: path.join(dir, "sessions.json") }; - } + const storeFixture = createSlackSessionStoreFixture("openclaw-slack-explicit-mention-"); beforeAll(() => { - fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-explicit-mention-")); + storeFixture.setup(); }); afterAll(() => { - if (fixtureRoot) { - fs.rmSync(fixtureRoot, { recursive: true, force: true }); - fixtureRoot = ""; - } + storeFixture.cleanup(); }); function createCtxWithExplicitMention(requireExplicitMention: boolean) { @@ -750,7 +726,7 @@ describe("slack thread.requireExplicitMention", () => { it("drops thread reply without explicit mention when requireExplicitMention is true", async () => { const ctx = createCtxWithExplicitMention(true); - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); vi.spyOn( await import("openclaw/plugin-sdk/config-runtime"), "resolveStorePath", @@ -777,7 +753,7 @@ describe("slack thread.requireExplicitMention", () => { it("allows thread reply with explicit @mention when requireExplicitMention is true", async () => { const ctx = createCtxWithExplicitMention(true); - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); vi.spyOn( await import("openclaw/plugin-sdk/config-runtime"), "resolveStorePath", @@ -804,7 +780,7 @@ describe("slack thread.requireExplicitMention", () => { it("allows thread reply without explicit mention when requireExplicitMention is false (default)", async () => { const ctx = createCtxWithExplicitMention(false); - const { storePath } = makeTmpStorePath(); + const { storePath } = storeFixture.makeTmpStorePath(); vi.spyOn( await import("openclaw/plugin-sdk/config-runtime"), "resolveStorePath",