mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:14:58 +00:00
slack: keep top-level off-mode channel turns in one session
This commit is contained in:
committed by
Peter Steinberger
parent
cc18e43832
commit
29342c37b5
@@ -1,7 +1,6 @@
|
|||||||
import type { App } from "@slack/bolt";
|
import type { App } from "@slack/bolt";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { OpenClawConfig } from "../../../config/config.js";
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
import type { ResolvedSlackAccount } from "../../accounts.js";
|
|
||||||
import type { SlackMessageEvent } from "../../types.js";
|
import type { SlackMessageEvent } from "../../types.js";
|
||||||
import { prepareSlackMessage } from "./prepare.js";
|
import { prepareSlackMessage } from "./prepare.js";
|
||||||
import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js";
|
import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js";
|
||||||
@@ -20,48 +19,55 @@ function buildCtx(overrides?: { replyToMode?: "all" | "first" | "off" }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const account: ResolvedSlackAccount = createSlackTestAccount();
|
function buildChannelMessage(overrides?: Partial<SlackMessageEvent>): SlackMessageEvent {
|
||||||
|
return {
|
||||||
|
channel: "C123",
|
||||||
|
channel_type: "channel",
|
||||||
|
user: "U1",
|
||||||
|
text: "hello",
|
||||||
|
ts: "1770408518.451689",
|
||||||
|
...overrides,
|
||||||
|
} as SlackMessageEvent;
|
||||||
|
}
|
||||||
|
|
||||||
describe("thread-level session keys", () => {
|
describe("thread-level session keys", () => {
|
||||||
it("uses thread-level session key for channel messages", async () => {
|
it("keeps top-level channel turns in one session when replyToMode=off", async () => {
|
||||||
const ctx = buildCtx();
|
const ctx = buildCtx({ replyToMode: "off" });
|
||||||
ctx.resolveUserName = async () => ({ name: "Alice" });
|
ctx.resolveUserName = async () => ({ name: "Alice" });
|
||||||
|
const account = createSlackTestAccount({ replyToMode: "off" });
|
||||||
|
|
||||||
const message: SlackMessageEvent = {
|
const first = await prepareSlackMessage({
|
||||||
channel: "C123",
|
|
||||||
channel_type: "channel",
|
|
||||||
user: "U1",
|
|
||||||
text: "hello",
|
|
||||||
ts: "1770408518.451689",
|
|
||||||
} as SlackMessageEvent;
|
|
||||||
|
|
||||||
const prepared = await prepareSlackMessage({
|
|
||||||
ctx,
|
ctx,
|
||||||
account,
|
account,
|
||||||
message,
|
message: buildChannelMessage({ ts: "1770408518.451689" }),
|
||||||
|
opts: { source: "message" },
|
||||||
|
});
|
||||||
|
const second = await prepareSlackMessage({
|
||||||
|
ctx,
|
||||||
|
account,
|
||||||
|
message: buildChannelMessage({ ts: "1770408520.000001" }),
|
||||||
opts: { source: "message" },
|
opts: { source: "message" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prepared).toBeTruthy();
|
expect(first).toBeTruthy();
|
||||||
// Channel messages should get thread-level session key with :thread: suffix
|
expect(second).toBeTruthy();
|
||||||
// The resolved session key is in ctxPayload.SessionKey, not route.sessionKey
|
const firstSessionKey = first!.ctxPayload.SessionKey as string;
|
||||||
const sessionKey = prepared!.ctxPayload.SessionKey as string;
|
const secondSessionKey = second!.ctxPayload.SessionKey as string;
|
||||||
expect(sessionKey).toContain(":thread:");
|
expect(firstSessionKey).toBe(secondSessionKey);
|
||||||
expect(sessionKey).toContain("1770408518.451689");
|
expect(firstSessionKey).not.toContain(":thread:");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses parent thread_ts for thread replies", async () => {
|
it("uses parent thread_ts for thread replies even when replyToMode=off", async () => {
|
||||||
const ctx = buildCtx();
|
const ctx = buildCtx({ replyToMode: "off" });
|
||||||
ctx.resolveUserName = async () => ({ name: "Bob" });
|
ctx.resolveUserName = async () => ({ name: "Bob" });
|
||||||
|
const account = createSlackTestAccount({ replyToMode: "off" });
|
||||||
|
|
||||||
const message: SlackMessageEvent = {
|
const message = buildChannelMessage({
|
||||||
channel: "C123",
|
|
||||||
channel_type: "channel",
|
|
||||||
user: "U2",
|
user: "U2",
|
||||||
text: "reply",
|
text: "reply",
|
||||||
ts: "1770408522.168859",
|
ts: "1770408522.168859",
|
||||||
thread_ts: "1770408518.451689",
|
thread_ts: "1770408518.451689",
|
||||||
} as SlackMessageEvent;
|
});
|
||||||
|
|
||||||
const prepared = await prepareSlackMessage({
|
const prepared = await prepareSlackMessage({
|
||||||
ctx,
|
ctx,
|
||||||
@@ -77,9 +83,27 @@ describe("thread-level session keys", () => {
|
|||||||
expect(sessionKey).not.toContain("1770408522.168859");
|
expect(sessionKey).not.toContain("1770408522.168859");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not add thread suffix for DMs", async () => {
|
it("keeps top-level channel turns thread-scoped when replyToMode=all", async () => {
|
||||||
const ctx = buildCtx();
|
const ctx = buildCtx({ replyToMode: "all" });
|
||||||
ctx.resolveUserName = async () => ({ name: "Carol" });
|
ctx.resolveUserName = async () => ({ name: "Carol" });
|
||||||
|
const account = createSlackTestAccount({ replyToMode: "all" });
|
||||||
|
|
||||||
|
const prepared = await prepareSlackMessage({
|
||||||
|
ctx,
|
||||||
|
account,
|
||||||
|
message: buildChannelMessage({ ts: "1770408530.000000" }),
|
||||||
|
opts: { source: "message" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prepared).toBeTruthy();
|
||||||
|
const sessionKey = prepared!.ctxPayload.SessionKey as string;
|
||||||
|
expect(sessionKey).toContain(":thread:1770408530.000000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not add thread suffix for DMs when replyToMode=off", async () => {
|
||||||
|
const ctx = buildCtx({ replyToMode: "off" });
|
||||||
|
ctx.resolveUserName = async () => ({ name: "Carol" });
|
||||||
|
const account = createSlackTestAccount({ replyToMode: "off" });
|
||||||
|
|
||||||
const message: SlackMessageEvent = {
|
const message: SlackMessageEvent = {
|
||||||
channel: "D456",
|
channel: "D456",
|
||||||
|
|||||||
@@ -282,17 +282,20 @@ function resolveSlackRoutingContext(params: {
|
|||||||
const threadContext = resolveSlackThreadContext({ message, replyToMode });
|
const threadContext = resolveSlackThreadContext({ message, replyToMode });
|
||||||
const threadTs = threadContext.incomingThreadTs;
|
const threadTs = threadContext.incomingThreadTs;
|
||||||
const isThreadReply = threadContext.isThreadReply;
|
const isThreadReply = threadContext.isThreadReply;
|
||||||
// Keep channel/group sessions thread-scoped to avoid cross-thread context bleed.
|
// Keep true thread replies thread-scoped, but preserve channel-level sessions
|
||||||
|
// for top-level room turns when replyToMode is off.
|
||||||
// For DMs, preserve existing auto-thread behavior when replyToMode="all".
|
// For DMs, preserve existing auto-thread behavior when replyToMode="all".
|
||||||
const autoThreadId =
|
const autoThreadId =
|
||||||
!isThreadReply && replyToMode === "all" && threadContext.messageTs
|
!isThreadReply && replyToMode === "all" && threadContext.messageTs
|
||||||
? threadContext.messageTs
|
? threadContext.messageTs
|
||||||
: undefined;
|
: undefined;
|
||||||
const canonicalThreadId = isRoomish
|
const roomThreadId =
|
||||||
? (threadContext.incomingThreadTs ?? message.ts)
|
isThreadReply && threadTs
|
||||||
: isThreadReply
|
|
||||||
? threadTs
|
? threadTs
|
||||||
: autoThreadId;
|
: replyToMode === "off"
|
||||||
|
? undefined
|
||||||
|
: threadContext.messageTs;
|
||||||
|
const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId;
|
||||||
const threadKeys = resolveThreadSessionKeys({
|
const threadKeys = resolveThreadSessionKeys({
|
||||||
baseSessionKey: route.sessionKey,
|
baseSessionKey: route.sessionKey,
|
||||||
threadId: canonicalThreadId,
|
threadId: canonicalThreadId,
|
||||||
|
|||||||
Reference in New Issue
Block a user