diff --git a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts new file mode 100644 index 000000000..49ae73233 --- /dev/null +++ b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts @@ -0,0 +1,142 @@ +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; +import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk"; +import { describe, expect, it, vi } from "vitest"; +import { createMatrixRoomMessageHandler } from "./handler.js"; +import { EventType, type MatrixRawEvent } from "./types.js"; + +describe("createMatrixRoomMessageHandler BodyForAgent sender label", () => { + it("stores sender-labeled BodyForAgent for group thread messages", async () => { + const recordInboundSession = vi.fn().mockResolvedValue(undefined); + const formatInboundEnvelope = vi + .fn() + .mockImplementation((params: { senderLabel?: string; body: string }) => params.body); + const finalizeInboundContext = vi + .fn() + .mockImplementation((ctx: Record) => ctx); + + const core = { + channel: { + pairing: { + readAllowFromStore: vi.fn().mockResolvedValue([]), + }, + routing: { + resolveAgentRoute: vi.fn().mockReturnValue({ + agentId: "main", + accountId: undefined, + sessionKey: "agent:main:matrix:channel:!room:example.org", + mainSessionKey: "agent:main:main", + }), + }, + session: { + resolveStorePath: vi.fn().mockReturnValue("/tmp/openclaw-test-session.json"), + readSessionUpdatedAt: vi.fn().mockReturnValue(123), + recordInboundSession, + }, + reply: { + resolveEnvelopeFormatOptions: vi.fn().mockReturnValue({}), + formatInboundEnvelope, + formatAgentEnvelope: vi + .fn() + .mockImplementation((params: { body: string }) => params.body), + finalizeInboundContext, + resolveHumanDelayConfig: vi.fn().mockReturnValue(undefined), + createReplyDispatcherWithTyping: vi.fn().mockReturnValue({ + dispatcher: {}, + replyOptions: {}, + markDispatchIdle: vi.fn(), + }), + withReplyDispatcher: vi + .fn() + .mockResolvedValue({ queuedFinal: false, counts: { final: 0, partial: 0, tool: 0 } }), + }, + commands: { + shouldHandleTextCommands: vi.fn().mockReturnValue(true), + }, + text: { + hasControlCommand: vi.fn().mockReturnValue(false), + resolveMarkdownTableMode: vi.fn().mockReturnValue("code"), + }, + }, + system: { + enqueueSystemEvent: vi.fn(), + }, + } as unknown as PluginRuntime; + + const runtime = { + error: vi.fn(), + } as unknown as RuntimeEnv; + const logger = { + info: vi.fn(), + warn: vi.fn(), + } as unknown as RuntimeLogger; + const logVerboseMessage = vi.fn(); + + const client = { + getUserId: vi.fn().mockResolvedValue("@bot:matrix.example.org"), + } as unknown as MatrixClient; + + const handler = createMatrixRoomMessageHandler({ + client, + core, + cfg: {}, + runtime, + logger, + logVerboseMessage, + allowFrom: [], + roomsConfig: undefined, + mentionRegexes: [], + groupPolicy: "open", + replyToMode: "first", + threadReplies: "inbound", + dmEnabled: true, + dmPolicy: "open", + textLimit: 4000, + mediaMaxBytes: 5 * 1024 * 1024, + startupMs: Date.now(), + startupGraceMs: 60_000, + directTracker: { + isDirectMessage: vi.fn().mockResolvedValue(false), + }, + getRoomInfo: vi.fn().mockResolvedValue({ + name: "Dev Room", + canonicalAlias: "#dev:matrix.example.org", + altAliases: [], + }), + getMemberDisplayName: vi.fn().mockResolvedValue("Bu"), + accountId: undefined, + }); + + const event = { + type: EventType.RoomMessage, + event_id: "$event1", + sender: "@bu:matrix.example.org", + origin_server_ts: Date.now(), + content: { + msgtype: "m.text", + body: "show me my commits", + "m.mentions": { user_ids: ["@bot:matrix.example.org"] }, + "m.relates_to": { + rel_type: "m.thread", + event_id: "$thread-root", + }, + }, + } as unknown as MatrixRawEvent; + + await handler("!room:example.org", event); + + expect(formatInboundEnvelope).toHaveBeenCalledWith( + expect.objectContaining({ + chatType: "channel", + senderLabel: "Bu (bu)", + }), + ); + expect(recordInboundSession).toHaveBeenCalledWith( + expect.objectContaining({ + ctx: expect.objectContaining({ + ChatType: "thread", + BodyForAgent: "Bu (bu): show me my commits", + }), + }), + ); + }); +}); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 088548c57..8682e707a 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -26,7 +26,11 @@ import { resolveMatrixAllowListMatch, resolveMatrixAllowListMatches, } from "./allowlist.js"; -import { resolveMatrixBodyForAgent } from "./inbound-body.js"; +import { + resolveMatrixBodyForAgent, + resolveMatrixInboundSenderLabel, + resolveMatrixSenderUsername, +} from "./inbound-body.js"; import { resolveMatrixLocation, type MatrixLocationPayload } from "./location.js"; import { downloadMatrixMedia } from "./media.js"; import { resolveMentions } from "./mentions.js"; @@ -216,7 +220,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam } const senderName = await getMemberDisplayName(roomId, senderId); - const senderUsername = senderId.split(":")[0]?.replace(/^@/, ""); + const senderUsername = resolveMatrixSenderUsername(senderId); + const senderLabel = resolveMatrixInboundSenderLabel({ + senderName, + senderId, + senderUsername, + }); const storeAllowFrom = isDirectMessage ? await readStoreAllowFromForDmPolicy({ provider: "matrix", @@ -531,7 +540,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam envelope: envelopeOptions, body: textWithId, chatType: isDirectMessage ? "direct" : "channel", - sender: { name: senderName, username: senderUsername }, + senderLabel, }); const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined; @@ -540,8 +549,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam BodyForAgent: resolveMatrixBodyForAgent({ isDirectMessage, bodyText, - senderName, - senderId, + senderLabel, }), RawBody: bodyText, CommandBody: bodyText, diff --git a/extensions/matrix/src/matrix/monitor/inbound-body.test.ts b/extensions/matrix/src/matrix/monitor/inbound-body.test.ts index de54e0e65..8b5c63c89 100644 --- a/extensions/matrix/src/matrix/monitor/inbound-body.test.ts +++ b/extensions/matrix/src/matrix/monitor/inbound-body.test.ts @@ -1,7 +1,27 @@ import { describe, expect, it } from "vitest"; -import { resolveMatrixBodyForAgent, resolveMatrixInboundSenderLabel } from "./inbound-body.js"; +import { + resolveMatrixBodyForAgent, + resolveMatrixInboundSenderLabel, + resolveMatrixSenderUsername, +} from "./inbound-body.js"; + +describe("resolveMatrixSenderUsername", () => { + it("extracts localpart without leading @", () => { + expect(resolveMatrixSenderUsername("@bu:matrix.example.org")).toBe("bu"); + }); +}); describe("resolveMatrixInboundSenderLabel", () => { + it("uses provided senderUsername when present", () => { + expect( + resolveMatrixInboundSenderLabel({ + senderName: "Bu", + senderId: "@bu:matrix.example.org", + senderUsername: "BU_CUSTOM", + }), + ).toBe("Bu (BU_CUSTOM)"); + }); + it("includes sender username when it differs from display name", () => { expect( resolveMatrixInboundSenderLabel({ @@ -36,8 +56,7 @@ describe("resolveMatrixBodyForAgent", () => { resolveMatrixBodyForAgent({ isDirectMessage: true, bodyText: "show me my commits", - senderName: "Bu", - senderId: "@bu:matrix.example.org", + senderLabel: "Bu (bu)", }), ).toBe("show me my commits"); }); @@ -47,8 +66,7 @@ describe("resolveMatrixBodyForAgent", () => { resolveMatrixBodyForAgent({ isDirectMessage: false, bodyText: "show me my commits", - senderName: "Bu", - senderId: "@bu:matrix.example.org", + senderLabel: "Bu (bu)", }), ).toBe("Bu (bu): show me my commits"); }); diff --git a/extensions/matrix/src/matrix/monitor/inbound-body.ts b/extensions/matrix/src/matrix/monitor/inbound-body.ts index 65b67417e..48ad8d31e 100644 --- a/extensions/matrix/src/matrix/monitor/inbound-body.ts +++ b/extensions/matrix/src/matrix/monitor/inbound-body.ts @@ -1,4 +1,4 @@ -function resolveMatrixSenderUsername(senderId: string): string | undefined { +export function resolveMatrixSenderUsername(senderId: string): string | undefined { const username = senderId.split(":")[0]?.replace(/^@/, "").trim(); return username ? username : undefined; } @@ -6,9 +6,10 @@ function resolveMatrixSenderUsername(senderId: string): string | undefined { export function resolveMatrixInboundSenderLabel(params: { senderName: string; senderId: string; + senderUsername?: string; }): string { const senderName = params.senderName.trim(); - const senderUsername = resolveMatrixSenderUsername(params.senderId); + const senderUsername = params.senderUsername ?? resolveMatrixSenderUsername(params.senderId); if (senderName && senderUsername && senderName !== senderUsername) { return `${senderName} (${senderUsername})`; } @@ -18,15 +19,10 @@ export function resolveMatrixInboundSenderLabel(params: { export function resolveMatrixBodyForAgent(params: { isDirectMessage: boolean; bodyText: string; - senderName: string; - senderId: string; + senderLabel: string; }): string { if (params.isDirectMessage) { return params.bodyText; } - const senderLabel = resolveMatrixInboundSenderLabel({ - senderName: params.senderName, - senderId: params.senderId, - }); - return `${senderLabel}: ${params.bodyText}`; + return `${params.senderLabel}: ${params.bodyText}`; }