Files
openclaw/extensions/matrix/src/matrix/monitor/replies.test.ts
Peter Steinberger 2514980118 feat(matrix): handle voice preflight and threads (#90415)
* feat(matrix): handle voice preflight and threads

Co-authored-by: Frank Dierolf <frank_dierolf@web.de>
Co-authored-by: marc.wilson <marcwilson@gazasrv15i5.globaladvisors.biz>

* test(matrix): satisfy ci guards

* fix(matrix): preserve thread relations on edits

* chore: annotate deprecated compatibility aliases

* fix(matrix): include poll thread roots in reads

* test(matrix): enable audio preflight qa config

* test(matrix): make voice preflight QA mention deterministic

---------

Co-authored-by: Frank Dierolf <frank_dierolf@web.de>
Co-authored-by: marc.wilson <marcwilson@gazasrv15i5.globaladvisors.biz>
2026-06-05 08:49:35 -07:00

259 lines
8.4 KiB
TypeScript

// Matrix tests cover replies plugin behavior.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PluginRuntime, RuntimeEnv } from "../../../runtime-api.js";
import type { MatrixClient } from "../sdk.js";
const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" }));
const chunkMatrixTextMock = vi.hoisted(() =>
vi.fn((text: string, _opts?: unknown) => ({
trimmedText: text.trim(),
convertedText: text,
singleEventLimit: 4000,
fitsInSingleEvent: true,
chunks: text ? [text] : [],
})),
);
vi.mock("../send.js", () => ({
chunkMatrixText: (text: string, opts?: unknown) => chunkMatrixTextMock(text, opts),
sendMessageMatrix: (to: string, message: string, opts?: unknown) =>
sendMessageMatrixMock(to, message, opts),
}));
import { setMatrixRuntime } from "../../runtime.js";
import { deliverMatrixReplies } from "./replies.js";
function sendCall(index: number) {
const call = sendMessageMatrixMock.mock.calls.at(index);
if (!call) {
throw new Error(`Expected send call at index ${index}`);
}
return call;
}
function sendOptions(index: number): Record<string, unknown> {
const options = sendCall(index)[2];
if (!options || typeof options !== "object") {
throw new Error(`Expected send options at call ${index}`);
}
return options as Record<string, unknown>;
}
describe("deliverMatrixReplies", () => {
const cfg = { channels: { matrix: {} } };
const loadConfigMock = vi.fn(() => ({}));
const resolveMarkdownTableModeMock = vi.fn<(params: unknown) => string>(() => "code");
const convertMarkdownTablesMock = vi.fn((text: string) => text);
const resolveChunkModeMock = vi.fn<
(cfg: unknown, channel: unknown, accountId?: unknown) => string
>(() => "length");
const chunkMarkdownTextWithModeMock = vi.fn((text: string) => [text]);
const runtimeStub = {
config: {
current: () => loadConfigMock(),
},
channel: {
text: {
resolveMarkdownTableMode: (params: unknown) => resolveMarkdownTableModeMock(params),
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
resolveChunkMode: (cfgLocal: unknown, channel: unknown, accountId?: unknown) =>
resolveChunkModeMock(cfgLocal, channel, accountId),
chunkMarkdownTextWithMode: (text: string) => chunkMarkdownTextWithModeMock(text),
},
},
logging: {
shouldLogVerbose: () => false,
},
} as unknown as PluginRuntime;
const runtimeEnv: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
} as unknown as RuntimeEnv;
beforeEach(() => {
vi.clearAllMocks();
setMatrixRuntime(runtimeStub);
chunkMatrixTextMock.mockReset().mockImplementation((text: string) => ({
trimmedText: text.trim(),
convertedText: text,
singleEventLimit: 4000,
fitsInSingleEvent: true,
chunks: text ? [text] : [],
}));
});
it("keeps replyToId on first reply only when replyToMode=first", async () => {
chunkMatrixTextMock.mockImplementation((text: string) => ({
trimmedText: text.trim(),
convertedText: text,
singleEventLimit: 4000,
fitsInSingleEvent: true,
chunks: text.split("|"),
}));
await deliverMatrixReplies({
cfg,
replies: [
{ text: "first-a|first-b", replyToId: "reply-1" },
{ text: "second", replyToId: "reply-2" },
],
roomId: "room:1",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "first",
});
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
expect(sendOptions(0).replyToId).toBe("reply-1");
expect(sendOptions(0).threadId).toBeUndefined();
expect(sendOptions(1).replyToId).toBe("reply-1");
expect(sendOptions(1).threadId).toBeUndefined();
expect(sendOptions(2).replyToId).toBeUndefined();
expect(sendOptions(2).threadId).toBeUndefined();
});
it("keeps replyToId on every reply when replyToMode=all", async () => {
await deliverMatrixReplies({
cfg,
replies: [
{
text: "caption",
mediaUrls: ["https://example.com/a.jpg", "https://example.com/b.jpg"],
replyToId: "reply-media",
audioAsVoice: true,
},
{ text: "plain", replyToId: "reply-text" },
],
roomId: "room:2",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "all",
mediaLocalRoots: ["/tmp/openclaw-matrix-test"],
});
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(3);
expect(sendCall(0)[0]).toBe("room:2");
expect(sendCall(0)[1]).toBe("caption");
expect(sendOptions(0).mediaUrl).toBe("https://example.com/a.jpg");
expect(sendOptions(0).mediaLocalRoots).toEqual(["/tmp/openclaw-matrix-test"]);
expect(sendOptions(0).replyToId).toBe("reply-media");
expect(sendCall(1)[0]).toBe("room:2");
expect(sendCall(1)[1]).toBe("");
expect(sendOptions(1).mediaUrl).toBe("https://example.com/b.jpg");
expect(sendOptions(1).mediaLocalRoots).toEqual(["/tmp/openclaw-matrix-test"]);
expect(sendOptions(1).replyToId).toBe("reply-media");
expect(sendOptions(2).replyToId).toBe("reply-text");
});
it("keeps replyToId when threadId is set so Matrix can send fallback metadata", async () => {
chunkMatrixTextMock.mockImplementation((text: string) => ({
trimmedText: text.trim(),
convertedText: text,
singleEventLimit: 4000,
fitsInSingleEvent: true,
chunks: text.split("|"),
}));
await deliverMatrixReplies({
cfg,
replies: [{ text: "hello|thread" }],
roomId: "room:3",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "off",
threadId: "thread-77",
replyToId: "reply-thread",
});
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(2);
expect(sendOptions(0).replyToId).toBe("reply-thread");
expect(sendOptions(0).threadId).toBe("thread-77");
expect(sendOptions(1).replyToId).toBe("reply-thread");
expect(sendOptions(1).threadId).toBe("thread-77");
});
it("suppresses reasoning-only text before Matrix sends", async () => {
await deliverMatrixReplies({
cfg,
replies: [
{ text: "Reasoning:\n_hidden_" },
{ text: "<think>still hidden</think>" },
{ text: "Visible answer" },
],
roomId: "room:5",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "off",
});
expect(sendMessageMatrixMock).toHaveBeenCalledTimes(1);
expect(sendCall(0)[0]).toBe("room:5");
expect(sendCall(0)[1]).toBe("Visible answer");
expect(sendOptions(0).cfg).toBe(cfg);
});
it("uses supplied cfg for chunking and send delivery without reloading runtime config", async () => {
const explicitCfg = {
channels: {
matrix: {
accounts: {
ops: {
chunkMode: "newline",
},
},
},
},
};
loadConfigMock.mockImplementation(() => {
throw new Error("deliverMatrixReplies should not reload runtime config when cfg is provided");
});
await deliverMatrixReplies({
cfg: explicitCfg,
replies: [{ text: "hello", replyToId: "reply-1" }],
roomId: "room:4",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "all",
accountId: "ops",
});
expect(loadConfigMock).not.toHaveBeenCalled();
expect(chunkMatrixTextMock).toHaveBeenCalledWith("hello", {
cfg: explicitCfg,
accountId: "ops",
tableMode: "code",
});
expect(sendCall(0)[0]).toBe("room:4");
expect(sendCall(0)[1]).toBe("hello");
expect(sendOptions(0).cfg).toBe(explicitCfg);
expect(sendOptions(0).accountId).toBe("ops");
expect(sendOptions(0).replyToId).toBe("reply-1");
});
it("passes raw media captions through to sendMessageMatrix without pre-converting them", async () => {
convertMarkdownTablesMock.mockImplementation((text: string) => `converted:${text}`);
await deliverMatrixReplies({
cfg,
replies: [{ text: "caption", mediaUrl: "https://example.com/a.jpg" }],
roomId: "room:6",
client: {} as MatrixClient,
runtime: runtimeEnv,
textLimit: 4000,
replyToMode: "off",
});
expect(sendCall(0)[0]).toBe("room:6");
expect(sendCall(0)[1]).toBe("caption");
expect(sendOptions(0).mediaUrl).toBe("https://example.com/a.jpg");
});
});