refactor: dedupe channel and gateway surfaces

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:12 +00:00
parent 9617ac9dd5
commit 9d30159fcd
44 changed files with 1072 additions and 1479 deletions

View File

@@ -36,6 +36,18 @@ function createContext(overrides?: {
} as Parameters<typeof createSlackMessageHandler>[0]["ctx"];
}
function createHandlerWithTracker(overrides?: {
markMessageSeen?: (channel: string | undefined, ts: string | undefined) => boolean;
}) {
const trackEvent = vi.fn();
const handler = createSlackMessageHandler({
ctx: createContext(overrides),
account: { accountId: "default" } as Parameters<typeof createSlackMessageHandler>[0]["account"],
trackEvent,
});
return { handler, trackEvent };
}
describe("createSlackMessageHandler", () => {
beforeEach(() => {
enqueueMock.mockClear();
@@ -68,14 +80,7 @@ describe("createSlackMessageHandler", () => {
});
it("does not track duplicate messages that are already seen", async () => {
const trackEvent = vi.fn();
const handler = createSlackMessageHandler({
ctx: createContext({ markMessageSeen: () => true }),
account: { accountId: "default" } as Parameters<
typeof createSlackMessageHandler
>[0]["account"],
trackEvent,
});
const { handler, trackEvent } = createHandlerWithTracker({ markMessageSeen: () => true });
await handler(
{
@@ -93,14 +98,7 @@ describe("createSlackMessageHandler", () => {
});
it("tracks accepted non-duplicate messages", async () => {
const trackEvent = vi.fn();
const handler = createSlackMessageHandler({
ctx: createContext(),
account: { accountId: "default" } as Parameters<
typeof createSlackMessageHandler
>[0]["account"],
trackEvent,
});
const { handler, trackEvent } = createHandlerWithTracker();
await handler(
{

View File

@@ -0,0 +1,68 @@
import type { App } from "@slack/bolt";
import type { OpenClawConfig } from "../../../config/config.js";
import type { RuntimeEnv } from "../../../runtime.js";
import type { ResolvedSlackAccount } from "../../accounts.js";
import { createSlackMonitorContext } from "../context.js";
export function createInboundSlackTestContext(params: {
cfg: OpenClawConfig;
appClient?: App["client"];
defaultRequireMention?: boolean;
replyToMode?: "off" | "all" | "first";
channelsConfig?: Record<string, { systemPrompt: string }>;
}) {
return createSlackMonitorContext({
cfg: params.cfg,
accountId: "default",
botToken: "token",
app: { client: params.appClient ?? {} } as App,
runtime: {} as RuntimeEnv,
botUserId: "B1",
teamId: "T1",
apiAppId: "A1",
historyLimit: 0,
sessionScope: "per-sender",
mainKey: "main",
dmEnabled: true,
dmPolicy: "open",
allowFrom: [],
allowNameMatching: false,
groupDmEnabled: true,
groupDmChannels: [],
defaultRequireMention: params.defaultRequireMention ?? true,
channelsConfig: params.channelsConfig,
groupPolicy: "open",
useAccessGroups: false,
reactionMode: "off",
reactionAllowlist: [],
replyToMode: params.replyToMode ?? "off",
threadHistoryScope: "thread",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 4000,
ackReactionScope: "group-mentions",
mediaMaxBytes: 1024,
removeAckAfterReply: false,
});
}
export function createSlackTestAccount(
config: ResolvedSlackAccount["config"] = {},
): ResolvedSlackAccount {
return {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config,
replyToMode: config.replyToMode,
replyToModeByChatType: config.replyToModeByChatType,
dm: config.dm,
};
}

View File

@@ -1,64 +1,26 @@
import type { App } from "@slack/bolt";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../../config/config.js";
import type { RuntimeEnv } from "../../../runtime.js";
import type { ResolvedSlackAccount } from "../../accounts.js";
import type { SlackMessageEvent } from "../../types.js";
import { createSlackMonitorContext } from "../context.js";
import { prepareSlackMessage } from "./prepare.js";
import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js";
function buildCtx(overrides?: { replyToMode?: "all" | "first" | "off" }) {
return createSlackMonitorContext({
const replyToMode = overrides?.replyToMode ?? "all";
return createInboundSlackTestContext({
cfg: {
channels: {
slack: { enabled: true, replyToMode: overrides?.replyToMode ?? "all" },
slack: { enabled: true, replyToMode },
},
} as OpenClawConfig,
accountId: "default",
botToken: "token",
app: { client: {} } as App,
runtime: {} as RuntimeEnv,
botUserId: "B1",
teamId: "T1",
apiAppId: "A1",
historyLimit: 0,
sessionScope: "per-sender",
mainKey: "main",
dmEnabled: true,
dmPolicy: "open",
allowFrom: [],
groupDmEnabled: true,
groupDmChannels: [],
appClient: {} as App["client"],
defaultRequireMention: false,
groupPolicy: "open",
allowNameMatching: false,
useAccessGroups: false,
reactionMode: "off",
reactionAllowlist: [],
replyToMode: overrides?.replyToMode ?? "all",
threadHistoryScope: "thread",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 4000,
ackReactionScope: "group-mentions",
mediaMaxBytes: 1024,
removeAckAfterReply: false,
replyToMode,
});
}
const account: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
userTokenSource: "none",
config: {},
};
const account: ResolvedSlackAccount = createSlackTestAccount();
describe("thread-level session keys", () => {
it("uses thread-level session key for channel messages", async () => {

View File

@@ -510,11 +510,11 @@ export async function registerSlackMonitorSlashCommands(params: {
const [
{ resolveConversationLabel },
{ createReplyPrefixOptions },
{ recordSessionMetaFromInbound, resolveStorePath },
{ recordInboundSessionMetaSafe },
] = await Promise.all([
import("../../channels/conversation-label.js"),
import("../../channels/reply-prefix.js"),
import("../../config/sessions.js"),
import("../../channels/session-meta.js"),
]);
const route = resolveAgentRoute({
@@ -578,18 +578,14 @@ export async function registerSlackMonitorSlashCommands(params: {
OriginatingTo: `user:${command.user_id}`,
});
const storePath = resolveStorePath(cfg.session?.store, {
await recordInboundSessionMetaSafe({
cfg,
agentId: route.agentId,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
onError: (err) =>
runtime.error?.(danger(`slack slash: failed updating session meta: ${String(err)}`)),
});
try {
await recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
});
} catch (err) {
runtime.error?.(danger(`slack slash: failed updating session meta: ${String(err)}`));
}
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
cfg,