mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 06:41:22 +00:00
refactor(channels): dedupe transport and gateway test scaffolds
This commit is contained in:
@@ -1,57 +1,31 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { buildDispatchInboundCaptureMock } from "../../../test/helpers/dispatch-inbound-capture.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
|
||||
let capturedCtx: MsgContext | undefined;
|
||||
|
||||
vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../auto-reply/dispatch.js")>();
|
||||
const dispatchInboundMessage = vi.fn(async (params: { ctx: MsgContext }) => {
|
||||
capturedCtx = params.ctx;
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||
return buildDispatchInboundCaptureMock(actual, (ctx) => {
|
||||
capturedCtx = ctx as MsgContext;
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
dispatchInboundMessage,
|
||||
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||
};
|
||||
});
|
||||
|
||||
import { createSignalEventHandler } from "./event-handler.js";
|
||||
import { createBaseSignalEventHandlerDeps } from "./event-handler.test-harness.js";
|
||||
|
||||
describe("signal createSignalEventHandler inbound contract", () => {
|
||||
it("passes a finalized MsgContext to dispatchInboundMessage", async () => {
|
||||
capturedCtx = undefined;
|
||||
|
||||
const handler = createSignalEventHandler({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
baseUrl: "http://localhost",
|
||||
accountId: "default",
|
||||
historyLimit: 0,
|
||||
groupHistories: new Map(),
|
||||
textLimit: 4000,
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["*"],
|
||||
groupPolicy: "open",
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
mediaMaxBytes: 1024,
|
||||
ignoreAttachments: true,
|
||||
sendReadReceipts: false,
|
||||
readReceiptsViaDaemon: false,
|
||||
fetchAttachment: async () => null,
|
||||
deliverReplies: async () => {},
|
||||
resolveSignalReactionTargets: () => [],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
isSignalReactionMessage: () => false as any,
|
||||
shouldEmitSignalReactionNotification: () => false,
|
||||
buildSignalReactionSystemEventText: () => "reaction",
|
||||
});
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
|
||||
historyLimit: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
await handler({
|
||||
event: "receive",
|
||||
|
||||
@@ -1,55 +1,20 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { MsgContext } from "../../auto-reply/templating.js";
|
||||
import { buildDispatchInboundCaptureMock } from "../../../test/helpers/dispatch-inbound-capture.js";
|
||||
import { createBaseSignalEventHandlerDeps } from "./event-handler.test-harness.js";
|
||||
|
||||
let capturedCtx: MsgContext | undefined;
|
||||
|
||||
vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../auto-reply/dispatch.js")>();
|
||||
const dispatchInboundMessage = vi.fn(async (params: { ctx: MsgContext }) => {
|
||||
capturedCtx = params.ctx;
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||
return buildDispatchInboundCaptureMock(actual, (ctx) => {
|
||||
capturedCtx = ctx as MsgContext;
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
dispatchInboundMessage,
|
||||
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||
};
|
||||
});
|
||||
|
||||
import { createSignalEventHandler } from "./event-handler.js";
|
||||
import { renderSignalMentions } from "./mentions.js";
|
||||
|
||||
function createBaseDeps(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
baseUrl: "http://localhost",
|
||||
accountId: "default",
|
||||
historyLimit: 5,
|
||||
groupHistories: new Map(),
|
||||
textLimit: 4000,
|
||||
dmPolicy: "open" as const,
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["*"],
|
||||
groupPolicy: "open" as const,
|
||||
reactionMode: "off" as const,
|
||||
reactionAllowlist: [],
|
||||
mediaMaxBytes: 1024,
|
||||
ignoreAttachments: true,
|
||||
sendReadReceipts: false,
|
||||
readReceiptsViaDaemon: false,
|
||||
fetchAttachment: async () => null,
|
||||
deliverReplies: async () => {},
|
||||
resolveSignalReactionTargets: () => [],
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
isSignalReactionMessage: () => false as any,
|
||||
shouldEmitSignalReactionNotification: () => false,
|
||||
buildSignalReactionSystemEventText: () => "reaction",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
type GroupEventOpts = {
|
||||
message?: string;
|
||||
attachments?: unknown[];
|
||||
@@ -82,11 +47,37 @@ function makeGroupEvent(opts: GroupEventOpts) {
|
||||
};
|
||||
}
|
||||
|
||||
function createMentionGatedHistoryHandler() {
|
||||
const groupHistories = new Map();
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
},
|
||||
historyLimit: 5,
|
||||
groupHistories,
|
||||
}),
|
||||
);
|
||||
return { handler, groupHistories };
|
||||
}
|
||||
|
||||
async function expectSkippedGroupHistory(opts: GroupEventOpts, expectedBody: string) {
|
||||
capturedCtx = undefined;
|
||||
const { handler, groupHistories } = createMentionGatedHistoryHandler();
|
||||
await handler(makeGroupEvent(opts));
|
||||
expect(capturedCtx).toBeUndefined();
|
||||
const entries = groupHistories.get("g1");
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].body).toBe(expectedBody);
|
||||
}
|
||||
|
||||
describe("signal mention gating", () => {
|
||||
it("drops group messages without mention when requireMention is configured", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
@@ -101,7 +92,7 @@ describe("signal mention gating", () => {
|
||||
it("allows group messages with mention when requireMention is configured", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
@@ -117,7 +108,7 @@ describe("signal mention gating", () => {
|
||||
it("sets WasMentioned=false for group messages without mention when requireMention is off", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: false } } } },
|
||||
@@ -132,75 +123,30 @@ describe("signal mention gating", () => {
|
||||
|
||||
it("records pending history for skipped group messages", async () => {
|
||||
capturedCtx = undefined;
|
||||
const groupHistories = new Map();
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
},
|
||||
historyLimit: 5,
|
||||
groupHistories,
|
||||
}),
|
||||
);
|
||||
|
||||
const { handler, groupHistories } = createMentionGatedHistoryHandler();
|
||||
await handler(makeGroupEvent({ message: "hello from alice" }));
|
||||
expect(capturedCtx).toBeUndefined();
|
||||
const entries = groupHistories.get("g1");
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].sender).toBe("Alice");
|
||||
expect(entries[0].body).toBe("hello from alice");
|
||||
});
|
||||
|
||||
it("records attachment placeholder in pending history for skipped attachment-only group messages", async () => {
|
||||
capturedCtx = undefined;
|
||||
const groupHistories = new Map();
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
},
|
||||
historyLimit: 5,
|
||||
groupHistories,
|
||||
}),
|
||||
await expectSkippedGroupHistory(
|
||||
{ message: "", attachments: [{ id: "a1" }] },
|
||||
"<media:attachment>",
|
||||
);
|
||||
|
||||
await handler(makeGroupEvent({ message: "", attachments: [{ id: "a1" }] }));
|
||||
expect(capturedCtx).toBeUndefined();
|
||||
const entries = groupHistories.get("g1");
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].body).toBe("<media:attachment>");
|
||||
});
|
||||
|
||||
it("records quote text in pending history for skipped quote-only group messages", async () => {
|
||||
capturedCtx = undefined;
|
||||
const groupHistories = new Map();
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
},
|
||||
historyLimit: 5,
|
||||
groupHistories,
|
||||
}),
|
||||
);
|
||||
|
||||
await handler(makeGroupEvent({ message: "", quoteText: "quoted context" }));
|
||||
expect(capturedCtx).toBeUndefined();
|
||||
const entries = groupHistories.get("g1");
|
||||
expect(entries).toBeTruthy();
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0].body).toBe("quoted context");
|
||||
await expectSkippedGroupHistory({ message: "", quoteText: "quoted context" }, "quoted context");
|
||||
});
|
||||
|
||||
it("bypasses mention gating for authorized control commands", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
@@ -215,7 +161,7 @@ describe("signal mention gating", () => {
|
||||
it("hydrates mention placeholders before trimming so offsets stay aligned", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@bot"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: false } } } },
|
||||
@@ -247,7 +193,7 @@ describe("signal mention gating", () => {
|
||||
it("counts mention metadata replacements toward requireMention gating", async () => {
|
||||
capturedCtx = undefined;
|
||||
const handler = createSignalEventHandler(
|
||||
createBaseDeps({
|
||||
createBaseSignalEventHandlerDeps({
|
||||
cfg: {
|
||||
messages: { inbound: { debounceMs: 0 }, groupChat: { mentionPatterns: ["@123e4567"] } },
|
||||
channels: { signal: { groups: { "*": { requireMention: true } } } },
|
||||
|
||||
35
src/signal/monitor/event-handler.test-harness.ts
Normal file
35
src/signal/monitor/event-handler.test-harness.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { SignalEventHandlerDeps, SignalReactionMessage } from "./event-handler.types.js";
|
||||
|
||||
export function createBaseSignalEventHandlerDeps(
|
||||
overrides: Partial<SignalEventHandlerDeps> = {},
|
||||
): SignalEventHandlerDeps {
|
||||
return {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
runtime: { log: () => {}, error: () => {} } as any,
|
||||
cfg: {},
|
||||
baseUrl: "http://localhost",
|
||||
accountId: "default",
|
||||
historyLimit: 5,
|
||||
groupHistories: new Map(),
|
||||
textLimit: 4000,
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groupAllowFrom: ["*"],
|
||||
groupPolicy: "open",
|
||||
reactionMode: "off",
|
||||
reactionAllowlist: [],
|
||||
mediaMaxBytes: 1024,
|
||||
ignoreAttachments: true,
|
||||
sendReadReceipts: false,
|
||||
readReceiptsViaDaemon: false,
|
||||
fetchAttachment: async () => null,
|
||||
deliverReplies: async () => {},
|
||||
resolveSignalReactionTargets: () => [],
|
||||
isSignalReactionMessage: (
|
||||
_reaction: SignalReactionMessage | null | undefined,
|
||||
): _reaction is SignalReactionMessage => false,
|
||||
shouldEmitSignalReactionNotification: () => false,
|
||||
buildSignalReactionSystemEventText: () => "reaction",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user