mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
test(slack): share thread message store fixtures
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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") };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user