mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(outbound): unify resolved cfg threading across send paths (#33987)
This commit is contained in:
@@ -34,6 +34,7 @@ const loadWebMediaMock = vi.fn().mockResolvedValue({
|
||||
contentType: "image/png",
|
||||
kind: "image",
|
||||
});
|
||||
const runtimeLoadConfigMock = vi.fn(() => ({}));
|
||||
const mediaKindFromMimeMock = vi.fn(() => "image");
|
||||
const isVoiceCompatibleAudioMock = vi.fn(() => false);
|
||||
const getImageMetadataMock = vi.fn().mockResolvedValue(null);
|
||||
@@ -41,7 +42,7 @@ const resizeToJpegMock = vi.fn();
|
||||
|
||||
const runtimeStub = {
|
||||
config: {
|
||||
loadConfig: () => ({}),
|
||||
loadConfig: runtimeLoadConfigMock,
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: loadWebMediaMock as unknown as PluginRuntime["media"]["loadWebMedia"],
|
||||
@@ -65,6 +66,7 @@ const runtimeStub = {
|
||||
} as unknown as PluginRuntime;
|
||||
|
||||
let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix;
|
||||
let resolveMediaMaxBytes: typeof import("./send/client.js").resolveMediaMaxBytes;
|
||||
|
||||
const makeClient = () => {
|
||||
const sendMessage = vi.fn().mockResolvedValue("evt1");
|
||||
@@ -80,11 +82,14 @@ const makeClient = () => {
|
||||
beforeAll(async () => {
|
||||
setMatrixRuntime(runtimeStub);
|
||||
({ sendMessageMatrix } = await import("./send.js"));
|
||||
({ resolveMediaMaxBytes } = await import("./send/client.js"));
|
||||
});
|
||||
|
||||
describe("sendMessageMatrix media", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
runtimeLoadConfigMock.mockReset();
|
||||
runtimeLoadConfigMock.mockReturnValue({});
|
||||
mediaKindFromMimeMock.mockReturnValue("image");
|
||||
isVoiceCompatibleAudioMock.mockReturnValue(false);
|
||||
setMatrixRuntime(runtimeStub);
|
||||
@@ -214,6 +219,8 @@ describe("sendMessageMatrix media", () => {
|
||||
describe("sendMessageMatrix threads", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
runtimeLoadConfigMock.mockReset();
|
||||
runtimeLoadConfigMock.mockReturnValue({});
|
||||
setMatrixRuntime(runtimeStub);
|
||||
});
|
||||
|
||||
@@ -240,3 +247,80 @@ describe("sendMessageMatrix threads", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMessageMatrix cfg threading", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
runtimeLoadConfigMock.mockReset();
|
||||
runtimeLoadConfigMock.mockReturnValue({
|
||||
channels: {
|
||||
matrix: {
|
||||
mediaMaxMb: 7,
|
||||
},
|
||||
},
|
||||
});
|
||||
setMatrixRuntime(runtimeStub);
|
||||
});
|
||||
|
||||
it("does not call runtime loadConfig when cfg is provided", async () => {
|
||||
const { client } = makeClient();
|
||||
const providedCfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
mediaMaxMb: 4,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await sendMessageMatrix("room:!room:example", "hello cfg", {
|
||||
client,
|
||||
cfg: providedCfg as any,
|
||||
});
|
||||
|
||||
expect(runtimeLoadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to runtime loadConfig when cfg is omitted", async () => {
|
||||
const { client } = makeClient();
|
||||
|
||||
await sendMessageMatrix("room:!room:example", "hello runtime", { client });
|
||||
|
||||
expect(runtimeLoadConfigMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMediaMaxBytes cfg threading", () => {
|
||||
beforeEach(() => {
|
||||
runtimeLoadConfigMock.mockReset();
|
||||
runtimeLoadConfigMock.mockReturnValue({
|
||||
channels: {
|
||||
matrix: {
|
||||
mediaMaxMb: 9,
|
||||
},
|
||||
},
|
||||
});
|
||||
setMatrixRuntime(runtimeStub);
|
||||
});
|
||||
|
||||
it("uses provided cfg and skips runtime loadConfig", () => {
|
||||
const providedCfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
mediaMaxMb: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const maxBytes = resolveMediaMaxBytes(undefined, providedCfg as any);
|
||||
|
||||
expect(maxBytes).toBe(3 * 1024 * 1024);
|
||||
expect(runtimeLoadConfigMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to runtime loadConfig when cfg is omitted", () => {
|
||||
const maxBytes = resolveMediaMaxBytes();
|
||||
|
||||
expect(maxBytes).toBe(9 * 1024 * 1024);
|
||||
expect(runtimeLoadConfigMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,11 +47,12 @@ export async function sendMessageMatrix(
|
||||
client: opts.client,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
cfg: opts.cfg,
|
||||
});
|
||||
const cfg = opts.cfg ?? getCore().config.loadConfig();
|
||||
try {
|
||||
const roomId = await resolveMatrixRoomId(client, to);
|
||||
return await enqueueSend(roomId, async () => {
|
||||
const cfg = getCore().config.loadConfig();
|
||||
const tableMode = getCore().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "matrix",
|
||||
@@ -81,7 +82,7 @@ export async function sendMessageMatrix(
|
||||
|
||||
let lastMessageId = "";
|
||||
if (opts.mediaUrl) {
|
||||
const maxBytes = resolveMediaMaxBytes(opts.accountId);
|
||||
const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg);
|
||||
const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
|
||||
const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
|
||||
contentType: media.contentType,
|
||||
@@ -171,6 +172,7 @@ export async function sendPollMatrix(
|
||||
client: opts.client,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId: opts.accountId,
|
||||
cfg: opts.cfg,
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -32,19 +32,19 @@ function findAccountConfig(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveMediaMaxBytes(accountId?: string): number | undefined {
|
||||
const cfg = getCore().config.loadConfig() as CoreConfig;
|
||||
export function resolveMediaMaxBytes(accountId?: string, cfg?: CoreConfig): number | undefined {
|
||||
const resolvedCfg = cfg ?? (getCore().config.loadConfig() as CoreConfig);
|
||||
// Check account-specific config first (case-insensitive key matching)
|
||||
const accountConfig = findAccountConfig(
|
||||
cfg.channels?.matrix?.accounts as Record<string, unknown> | undefined,
|
||||
resolvedCfg.channels?.matrix?.accounts as Record<string, unknown> | undefined,
|
||||
accountId ?? "",
|
||||
);
|
||||
if (typeof accountConfig?.mediaMaxMb === "number") {
|
||||
return (accountConfig.mediaMaxMb as number) * 1024 * 1024;
|
||||
}
|
||||
// Fall back to top-level config
|
||||
if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") {
|
||||
return cfg.channels.matrix.mediaMaxMb * 1024 * 1024;
|
||||
if (typeof resolvedCfg.channels?.matrix?.mediaMaxMb === "number") {
|
||||
return resolvedCfg.channels.matrix.mediaMaxMb * 1024 * 1024;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -53,6 +53,7 @@ export async function resolveMatrixClient(opts: {
|
||||
client?: MatrixClient;
|
||||
timeoutMs?: number;
|
||||
accountId?: string;
|
||||
cfg?: CoreConfig;
|
||||
}): Promise<{ client: MatrixClient; stopOnDone: boolean }> {
|
||||
ensureNodeRuntime();
|
||||
if (opts.client) {
|
||||
@@ -84,10 +85,11 @@ export async function resolveMatrixClient(opts: {
|
||||
const client = await resolveSharedMatrixClient({
|
||||
timeoutMs: opts.timeoutMs,
|
||||
accountId,
|
||||
cfg: opts.cfg,
|
||||
});
|
||||
return { client, stopOnDone: false };
|
||||
}
|
||||
const auth = await resolveMatrixAuth({ accountId });
|
||||
const auth = await resolveMatrixAuth({ accountId, cfg: opts.cfg });
|
||||
const client = await createPreparedMatrixClient({
|
||||
auth,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
|
||||
@@ -85,6 +85,7 @@ export type MatrixSendResult = {
|
||||
};
|
||||
|
||||
export type MatrixSendOpts = {
|
||||
cfg?: import("../../types.js").CoreConfig;
|
||||
client?: import("@vector-im/matrix-bot-sdk").MatrixClient;
|
||||
mediaUrl?: string;
|
||||
accountId?: string;
|
||||
|
||||
159
extensions/matrix/src/outbound.test.ts
Normal file
159
extensions/matrix/src/outbound.test.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
sendMessageMatrix: vi.fn(),
|
||||
sendPollMatrix: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./matrix/send.js", () => ({
|
||||
sendMessageMatrix: mocks.sendMessageMatrix,
|
||||
sendPollMatrix: mocks.sendPollMatrix,
|
||||
}));
|
||||
|
||||
vi.mock("./runtime.js", () => ({
|
||||
getMatrixRuntime: () => ({
|
||||
channel: {
|
||||
text: {
|
||||
chunkMarkdownText: (text: string) => [text],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import { matrixOutbound } from "./outbound.js";
|
||||
|
||||
describe("matrixOutbound cfg threading", () => {
|
||||
beforeEach(() => {
|
||||
mocks.sendMessageMatrix.mockReset();
|
||||
mocks.sendPollMatrix.mockReset();
|
||||
mocks.sendMessageMatrix.mockResolvedValue({ messageId: "evt-1", roomId: "!room:example" });
|
||||
mocks.sendPollMatrix.mockResolvedValue({ eventId: "$poll", roomId: "!room:example" });
|
||||
});
|
||||
|
||||
it("passes resolved cfg to sendMessageMatrix for text sends", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "resolved-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await matrixOutbound.sendText!({
|
||||
cfg,
|
||||
to: "room:!room:example",
|
||||
text: "hello",
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
replyToId: "$reply",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessageMatrix).toHaveBeenCalledWith(
|
||||
"room:!room:example",
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
replyToId: "$reply",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes resolved cfg to sendMessageMatrix for media sends", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "resolved-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await matrixOutbound.sendMedia!({
|
||||
cfg,
|
||||
to: "room:!room:example",
|
||||
text: "caption",
|
||||
mediaUrl: "file:///tmp/cat.png",
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessageMatrix).toHaveBeenCalledWith(
|
||||
"room:!room:example",
|
||||
"caption",
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
mediaUrl: "file:///tmp/cat.png",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes resolved cfg through injected deps.sendMatrix", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "resolved-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const sendMatrix = vi.fn(async () => ({
|
||||
messageId: "evt-injected",
|
||||
roomId: "!room:example",
|
||||
}));
|
||||
|
||||
await matrixOutbound.sendText!({
|
||||
cfg,
|
||||
to: "room:!room:example",
|
||||
text: "hello via deps",
|
||||
deps: { sendMatrix },
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
replyToId: "$reply",
|
||||
});
|
||||
|
||||
expect(sendMatrix).toHaveBeenCalledWith(
|
||||
"room:!room:example",
|
||||
"hello via deps",
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
replyToId: "$reply",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes resolved cfg to sendPollMatrix", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "resolved-token",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await matrixOutbound.sendPoll!({
|
||||
cfg,
|
||||
to: "room:!room:example",
|
||||
poll: {
|
||||
question: "Snack?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
},
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
});
|
||||
|
||||
expect(mocks.sendPollMatrix).toHaveBeenCalledWith(
|
||||
"room:!room:example",
|
||||
expect.objectContaining({
|
||||
question: "Snack?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
cfg,
|
||||
accountId: "default",
|
||||
threadId: "$thread",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,11 +7,12 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4000,
|
||||
sendText: async ({ to, text, deps, replyToId, threadId, accountId }) => {
|
||||
sendText: async ({ cfg, to, text, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await send(to, text, {
|
||||
cfg,
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: resolvedThreadId,
|
||||
accountId: accountId ?? undefined,
|
||||
@@ -22,11 +23,12 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
roomId: result.roomId,
|
||||
};
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => {
|
||||
const send = deps?.sendMatrix ?? sendMessageMatrix;
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await send(to, text, {
|
||||
cfg,
|
||||
mediaUrl,
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: resolvedThreadId,
|
||||
@@ -38,10 +40,11 @@ export const matrixOutbound: ChannelOutboundAdapter = {
|
||||
roomId: result.roomId,
|
||||
};
|
||||
},
|
||||
sendPoll: async ({ to, poll, threadId, accountId }) => {
|
||||
sendPoll: async ({ cfg, to, poll, threadId, accountId }) => {
|
||||
const resolvedThreadId =
|
||||
threadId !== undefined && threadId !== null ? String(threadId) : undefined;
|
||||
const result = await sendPollMatrix(to, poll, {
|
||||
cfg,
|
||||
threadId: resolvedThreadId,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user