test(slack): share thread message store fixtures

This commit is contained in:
Vincent Koc
2026-04-12 05:23:26 +01:00
parent 3be7e3bde0
commit 1d1f10ecc2
3 changed files with 87 additions and 98 deletions

View File

@@ -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<Record<string, string>>;
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");

View File

@@ -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") };
},
};
}

View File

@@ -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",