openclaw/extensions/matrix-js/src/matrix/monitor/handler.test.ts

504 lines
14 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createMatrixRoomMessageHandler } from "./handler.js";
import { EventType, type MatrixRawEvent } from "./types.js";
const sendMessageMatrixMock = vi.hoisted(() =>
vi.fn(async (..._args: unknown[]) => ({ messageId: "evt", roomId: "!room" })),
);
vi.mock("../send.js", () => ({
reactMatrixMessage: vi.fn(async () => {}),
sendMessageMatrix: sendMessageMatrixMock,
sendReadReceiptMatrix: vi.fn(async () => {}),
sendTypingMatrix: vi.fn(async () => {}),
}));
function createReactionHarness(params?: {
cfg?: unknown;
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
allowFrom?: string[];
storeAllowFrom?: string[];
targetSender?: string;
isDirectMessage?: boolean;
senderName?: string;
}) {
const readAllowFromStore = vi.fn(async () => params?.storeAllowFrom ?? []);
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
const resolveAgentRoute = vi.fn(() => ({
agentId: "ops",
channel: "matrix-js",
accountId: "ops",
sessionKey: "agent:ops:main",
mainSessionKey: "agent:ops:main",
matchedBy: "binding.account",
}));
const enqueueSystemEvent = vi.fn();
const handler = createMatrixRoomMessageHandler({
client: {
getUserId: async () => "@bot:example.org",
getEvent: async () => ({ sender: params?.targetSender ?? "@bot:example.org" }),
} as never,
core: {
channel: {
pairing: {
readAllowFromStore,
upsertPairingRequest,
buildPairingReply: () => "pairing",
},
commands: {
shouldHandleTextCommands: () => false,
},
text: {
hasControlCommand: () => false,
},
routing: {
resolveAgentRoute,
},
},
system: {
enqueueSystemEvent,
},
} as never,
cfg: (params?.cfg ?? {}) as never,
accountId: "ops",
runtime: {
error: () => {},
} as never,
logger: {
info: () => {},
warn: () => {},
} as never,
logVerboseMessage: () => {},
allowFrom: params?.allowFrom ?? [],
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "off",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: params?.dmPolicy ?? "open",
textLimit: 8_000,
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
directTracker: {
isDirectMessage: async () => params?.isDirectMessage ?? true,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => params?.senderName ?? "sender",
});
return {
handler,
enqueueSystemEvent,
readAllowFromStore,
resolveAgentRoute,
upsertPairingRequest,
};
}
describe("matrix monitor handler pairing account scope", () => {
it("caches account-scoped allowFrom store reads on hot path", async () => {
const readAllowFromStore = vi.fn(async () => [] as string[]);
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
sendMessageMatrixMock.mockClear();
const handler = createMatrixRoomMessageHandler({
client: {
getUserId: async () => "@bot:example.org",
} as never,
core: {
channel: {
pairing: {
readAllowFromStore,
upsertPairingRequest,
buildPairingReply: () => "pairing",
},
},
} as never,
cfg: {} as never,
accountId: "ops",
runtime: {} as never,
logger: {
info: () => {},
warn: () => {},
} as never,
logVerboseMessage: () => {},
allowFrom: [],
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "off",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: "pairing",
textLimit: 8_000,
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
directTracker: {
isDirectMessage: async () => true,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => "sender",
});
await handler("!room:example.org", {
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: "$event1",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "hello",
"m.mentions": { room: true },
},
} as MatrixRawEvent);
await handler("!room:example.org", {
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: "$event2",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "hello again",
"m.mentions": { room: true },
},
} as MatrixRawEvent);
expect(readAllowFromStore).toHaveBeenCalledTimes(1);
});
it("sends pairing reminders for pending requests with cooldown", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-01T10:00:00.000Z"));
try {
const readAllowFromStore = vi.fn(async () => [] as string[]);
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
sendMessageMatrixMock.mockClear();
const handler = createMatrixRoomMessageHandler({
client: {
getUserId: async () => "@bot:example.org",
} as never,
core: {
channel: {
pairing: {
readAllowFromStore,
upsertPairingRequest,
buildPairingReply: () => "Pairing code: ABCDEFGH",
},
},
} as never,
cfg: {} as never,
accountId: "ops",
runtime: {} as never,
logger: {
info: () => {},
warn: () => {},
} as never,
logVerboseMessage: () => {},
allowFrom: [],
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "off",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: "pairing",
textLimit: 8_000,
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
directTracker: {
isDirectMessage: async () => true,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => "sender",
});
const makeEvent = (id: string): MatrixRawEvent =>
({
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: id,
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "hello",
"m.mentions": { room: true },
},
}) as MatrixRawEvent;
await handler("!room:example.org", makeEvent("$event1"));
await handler("!room:example.org", makeEvent("$event2"));
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
expect(String(sendMessageMatrixMock.mock.calls[0]?.[1] ?? "")).toContain(
"Pairing request is still pending approval.",
);
await vi.advanceTimersByTimeAsync(5 * 60_000 + 1);
await handler("!room:example.org", makeEvent("$event3"));
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
} finally {
vi.useRealTimers();
}
});
it("uses account-scoped pairing store reads and upserts for dm pairing", async () => {
const readAllowFromStore = vi.fn(async () => [] as string[]);
const upsertPairingRequest = vi.fn(async () => ({ code: "ABCDEFGH", created: false }));
const handler = createMatrixRoomMessageHandler({
client: {
getUserId: async () => "@bot:example.org",
} as never,
core: {
channel: {
pairing: {
readAllowFromStore,
upsertPairingRequest,
},
},
} as never,
cfg: {} as never,
accountId: "ops",
runtime: {} as never,
logger: {
info: () => {},
warn: () => {},
} as never,
logVerboseMessage: () => {},
allowFrom: [],
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "off",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: "pairing",
textLimit: 8_000,
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
directTracker: {
isDirectMessage: async () => true,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => "sender",
});
await handler("!room:example.org", {
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: "$event1",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "hello",
"m.mentions": { room: true },
},
} as MatrixRawEvent);
expect(readAllowFromStore).toHaveBeenCalledWith({
channel: "matrix-js",
env: process.env,
accountId: "ops",
});
expect(upsertPairingRequest).toHaveBeenCalledWith({
channel: "matrix-js",
id: "@user:example.org",
accountId: "ops",
meta: { name: "sender" },
});
});
it("passes accountId into route resolution for inbound dm messages", async () => {
const resolveAgentRoute = vi.fn(() => ({
agentId: "ops",
channel: "matrix-js",
accountId: "ops",
sessionKey: "agent:ops:main",
mainSessionKey: "agent:ops:main",
matchedBy: "binding.account",
}));
const handler = createMatrixRoomMessageHandler({
client: {
getUserId: async () => "@bot:example.org",
} as never,
core: {
channel: {
pairing: {
readAllowFromStore: async () => [] as string[],
upsertPairingRequest: async () => ({ code: "ABCDEFGH", created: false }),
},
commands: {
shouldHandleTextCommands: () => false,
},
text: {
hasControlCommand: () => false,
},
routing: {
resolveAgentRoute,
},
},
} as never,
cfg: {} as never,
accountId: "ops",
runtime: {
error: () => {},
} as never,
logger: {
info: () => {},
warn: () => {},
} as never,
logVerboseMessage: () => {},
allowFrom: [],
mentionRegexes: [],
groupPolicy: "open",
replyToMode: "off",
threadReplies: "inbound",
dmEnabled: true,
dmPolicy: "open",
textLimit: 8_000,
mediaMaxBytes: 10_000_000,
startupMs: 0,
startupGraceMs: 0,
directTracker: {
isDirectMessage: async () => true,
},
getRoomInfo: async () => ({ altAliases: [] }),
getMemberDisplayName: async () => "sender",
});
await handler("!room:example.org", {
type: EventType.RoomMessage,
sender: "@user:example.org",
event_id: "$event2",
origin_server_ts: Date.now(),
content: {
msgtype: "m.text",
body: "hello",
"m.mentions": { room: true },
},
} as MatrixRawEvent);
expect(resolveAgentRoute).toHaveBeenCalledWith(
expect.objectContaining({
channel: "matrix-js",
accountId: "ops",
}),
);
});
it("enqueues system events for reactions on bot-authored messages", async () => {
const { handler, enqueueSystemEvent, resolveAgentRoute } = createReactionHarness();
await handler("!room:example.org", {
type: EventType.Reaction,
sender: "@user:example.org",
event_id: "$reaction1",
origin_server_ts: Date.now(),
content: {
"m.relates_to": {
rel_type: "m.annotation",
event_id: "$msg1",
key: "👍",
},
},
} as MatrixRawEvent);
expect(resolveAgentRoute).toHaveBeenCalledWith(
expect.objectContaining({
channel: "matrix-js",
accountId: "ops",
}),
);
expect(enqueueSystemEvent).toHaveBeenCalledWith(
"Matrix reaction added: 👍 by sender on msg $msg1",
{
sessionKey: "agent:ops:main",
contextKey: "matrix:reaction:add:!room:example.org:$msg1:@user:example.org:👍",
},
);
});
it("ignores reactions that do not target bot-authored messages", async () => {
const { handler, enqueueSystemEvent, resolveAgentRoute } = createReactionHarness({
targetSender: "@other:example.org",
});
await handler("!room:example.org", {
type: EventType.Reaction,
sender: "@user:example.org",
event_id: "$reaction2",
origin_server_ts: Date.now(),
content: {
"m.relates_to": {
rel_type: "m.annotation",
event_id: "$msg2",
key: "👀",
},
},
} as MatrixRawEvent);
expect(enqueueSystemEvent).not.toHaveBeenCalled();
expect(resolveAgentRoute).not.toHaveBeenCalled();
});
it("does not create pairing requests for unauthorized dm reactions", async () => {
const { handler, enqueueSystemEvent, upsertPairingRequest } = createReactionHarness({
dmPolicy: "pairing",
});
await handler("!room:example.org", {
type: EventType.Reaction,
sender: "@user:example.org",
event_id: "$reaction3",
origin_server_ts: Date.now(),
content: {
"m.relates_to": {
rel_type: "m.annotation",
event_id: "$msg3",
key: "🔥",
},
},
} as MatrixRawEvent);
expect(upsertPairingRequest).not.toHaveBeenCalled();
expect(enqueueSystemEvent).not.toHaveBeenCalled();
});
it("honors account-scoped reaction notification overrides", async () => {
const { handler, enqueueSystemEvent } = createReactionHarness({
cfg: {
channels: {
"matrix-js": {
reactionNotifications: "own",
accounts: {
ops: {
reactionNotifications: "off",
},
},
},
},
},
});
await handler("!room:example.org", {
type: EventType.Reaction,
sender: "@user:example.org",
event_id: "$reaction4",
origin_server_ts: Date.now(),
content: {
"m.relates_to": {
rel_type: "m.annotation",
event_id: "$msg4",
key: "✅",
},
},
} as MatrixRawEvent);
expect(enqueueSystemEvent).not.toHaveBeenCalled();
});
});