import { beforeEach, describe, expect, it, vi } from "vitest" ;
import { handleMatrixAction } from "./tool-actions.js" ;
import type { CoreConfig } from "./types.js" ;
const mocks = vi.hoisted(() => ({
voteMatrixPoll: vi.fn(),
reactMatrixMessage: vi.fn(),
listMatrixReactions: vi.fn(),
removeMatrixReactions: vi.fn(),
sendMatrixMessage: vi.fn(),
listMatrixPins: vi.fn(),
getMatrixMemberInfo: vi.fn(),
getMatrixRoomInfo: vi.fn(),
applyMatrixProfileUpdate: vi.fn(),
}));
vi.mock("./matrix/actions.js" , () => {
return {
getMatrixMemberInfo: mocks.getMatrixMemberInfo,
getMatrixRoomInfo: mocks.getMatrixRoomInfo,
listMatrixReactions: mocks.listMatrixReactions,
listMatrixPins: mocks.listMatrixPins,
removeMatrixReactions: mocks.removeMatrixReactions,
sendMatrixMessage: mocks.sendMatrixMessage,
voteMatrixPoll: mocks.voteMatrixPoll,
};
});
vi.mock("./matrix/send.js" , () => {
return {
reactMatrixMessage: mocks.reactMatrixMessage,
};
});
vi.mock("./profile-update.js" , () => ({
applyMatrixProfileUpdate: (...args: unknown[]) => mocks.applyMatrixProfileUpdate(...args),
}));
describe("handleMatrixAction pollVote" , () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.voteMatrixPoll.mockResolvedValue({
eventId: "evt-poll-vote" ,
roomId: "!room:example" ,
pollId: "$poll" ,
answerIds: ["a1" , "a2" ],
labels: ["Pizza" , "Sushi" ],
maxSelections: 2 ,
});
mocks.listMatrixReactions.mockResolvedValue([{ key: "" , count: 1 , users: ["@u:example" ] }]);
mocks.listMatrixPins.mockResolvedValue({ pinned: ["$pin" ], events: [] });
mocks.removeMatrixReactions.mockResolvedValue({ removed: 1 });
mocks.sendMatrixMessage.mockResolvedValue({
messageId: "$sent" ,
roomId: "!room:example" ,
});
mocks.getMatrixMemberInfo.mockResolvedValue({ userId: "@u:example" });
mocks.getMatrixRoomInfo.mockResolvedValue({ roomId: "!room:example" });
mocks.applyMatrixProfileUpdate.mockResolvedValue({
accountId: "ops" ,
displayName: "Ops Bot" ,
avatarUrl: "mxc://example/avatar",
profile: {
displayNameUpdated: true ,
avatarUpdated: true ,
resolvedAvatarUrl: "mxc://example/avatar",
uploadedAvatarSource: null ,
convertedAvatarFromHttp: false ,
},
configPath: "channels.matrix.accounts.ops" ,
});
});
it("parses snake_case vote params and forwards normalized selectors" , async () => {
const cfg = {} as CoreConfig;
const result = await handleMatrixAction(
{
action: "pollVote" ,
account_id: "main" ,
room_id: "!room:example" ,
poll_id: "$poll" ,
poll_option_id: "a1" ,
poll_option_ids: ["a2" , "" ],
poll_option_index: "2" ,
poll_option_indexes: ["1" , "bogus" ],
},
cfg,
);
expect(mocks.voteMatrixPoll).toHaveBeenCalledWith("!room:example" , "$poll" , {
cfg,
accountId: "main" ,
optionIds: ["a2" , "a1" ],
optionIndexes: [1 , 2 ],
});
expect(result.details).toMatchObject({
ok: true ,
result: {
eventId: "evt-poll-vote" ,
answerIds: ["a1" , "a2" ],
},
});
});
it("rejects missing poll ids" , async () => {
await expect(
handleMatrixAction(
{
action: "pollVote" ,
roomId: "!room:example" ,
pollOptionIndex: 1 ,
},
{} as CoreConfig,
),
).rejects.toThrow("pollId required" );
});
it("accepts messageId as a pollId alias for poll votes" , async () => {
const cfg = {} as CoreConfig;
await handleMatrixAction(
{
action: "pollVote" ,
roomId: "!room:example" ,
messageId: "$poll" ,
pollOptionIndex: 1 ,
},
cfg,
);
expect(mocks.voteMatrixPoll).toHaveBeenCalledWith("!room:example" , "$poll" , {
cfg,
optionIds: [],
optionIndexes: [1 ],
});
});
it("passes account-scoped opts to add reactions" , async () => {
const cfg = { channels: { matrix: { actions: { reactions: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "react" ,
accountId: "ops" ,
roomId: "!room:example" ,
messageId: "$msg" ,
emoji: "" ,
},
cfg,
);
expect(mocks.reactMatrixMessage).toHaveBeenCalledWith("!room:example" , "$msg" , "" , {
cfg,
accountId: "ops" ,
});
});
it("passes account-scoped opts to remove reactions" , async () => {
const cfg = { channels: { matrix: { actions: { reactions: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "react" ,
account_id: "ops" ,
room_id: "!room:example" ,
message_id: "$msg" ,
emoji: "" ,
remove: true ,
},
cfg,
);
expect(mocks.removeMatrixReactions).toHaveBeenCalledWith("!room:example" , "$msg" , {
cfg,
accountId: "ops" ,
emoji: "" ,
});
});
it("passes account-scoped opts and limit to reaction listing" , async () => {
const cfg = { channels: { matrix: { actions: { reactions: true } } } } as CoreConfig;
const result = await handleMatrixAction(
{
action: "reactions" ,
account_id: "ops" ,
room_id: "!room:example" ,
message_id: "$msg" ,
limit: "5" ,
},
cfg,
);
expect(mocks.listMatrixReactions).toHaveBeenCalledWith("!room:example" , "$msg" , {
cfg,
accountId: "ops" ,
limit: 5 ,
});
expect(result.details).toMatchObject({
ok: true ,
reactions: [{ key: "" , count: 1 }],
});
});
it("passes account-scoped opts to message sends" , async () => {
const cfg = { channels: { matrix: { actions: { messages: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "sendMessage" ,
accountId: "ops" ,
to: "room:!room:example" ,
content: "hello" ,
threadId: "$thread" ,
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test" ] },
);
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example" , "hello" , {
cfg,
accountId: "ops" ,
mediaUrl: undefined,
mediaLocalRoots: ["/tmp/openclaw-matrix-test" ],
replyToId: undefined,
threadId: "$thread" ,
});
});
it("accepts media-only message sends" , async () => {
const cfg = { channels: { matrix: { actions: { messages: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "sendMessage" ,
accountId: "ops" ,
to: "room:!room:example" ,
mediaUrl: "file:///tmp/photo.png",
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test" ] },
);
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example" , undefined, {
cfg,
accountId: "ops" ,
mediaUrl: "file:///tmp/photo.png",
mediaLocalRoots: ["/tmp/openclaw-matrix-test" ],
replyToId: undefined,
threadId: undefined,
});
});
it("accepts shared media aliases and voice-send flags" , async () => {
const cfg = { channels: { matrix: { actions: { messages: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "sendMessage" ,
accountId: "ops" ,
to: "room:!room:example" ,
path: "/tmp/clip.mp3" ,
asVoice: true ,
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test" ] },
);
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example" , undefined, {
cfg,
accountId: "ops" ,
mediaUrl: "/tmp/clip.mp3" ,
mediaLocalRoots: ["/tmp/openclaw-matrix-test" ],
replyToId: undefined,
threadId: undefined,
audioAsVoice: true ,
});
});
it("passes mediaLocalRoots to profile updates" , async () => {
const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "setProfile" ,
accountId: "ops" ,
avatarPath: "/tmp/avatar.jpg" ,
},
cfg,
{ mediaLocalRoots: ["/tmp/openclaw-matrix-test" ] },
);
expect(mocks.applyMatrixProfileUpdate).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
account: "ops" ,
avatarPath: "/tmp/avatar.jpg" ,
mediaLocalRoots: ["/tmp/openclaw-matrix-test" ],
}),
);
});
it("passes account-scoped opts to pin listing" , async () => {
const cfg = { channels: { matrix: { actions: { pins: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "listPins" ,
accountId: "ops" ,
roomId: "!room:example" ,
},
cfg,
);
expect(mocks.listMatrixPins).toHaveBeenCalledWith("!room:example" , {
cfg,
accountId: "ops" ,
});
});
it("passes account-scoped opts to member and room info actions" , async () => {
const memberCfg = {
channels: { matrix: { actions: { memberInfo: true } } },
} as CoreConfig;
await handleMatrixAction(
{
action: "memberInfo" ,
accountId: "ops" ,
userId: "@u:example" ,
roomId: "!room:example" ,
},
memberCfg,
);
const roomCfg = { channels: { matrix: { actions: { channelInfo: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "channelInfo" ,
accountId: "ops" ,
roomId: "!room:example" ,
},
roomCfg,
);
expect(mocks.getMatrixMemberInfo).toHaveBeenCalledWith("@u:example" , {
cfg: memberCfg,
accountId: "ops" ,
roomId: "!room:example" ,
});
expect(mocks.getMatrixRoomInfo).toHaveBeenCalledWith("!room:example" , {
cfg: roomCfg,
accountId: "ops" ,
});
});
it("persists self-profile updates through the shared profile helper" , async () => {
const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig;
const result = await handleMatrixAction(
{
action: "setProfile" ,
account_id: "ops" ,
display_name: "Ops Bot" ,
avatar_url: "mxc://example/avatar",
},
cfg,
);
expect(mocks.applyMatrixProfileUpdate).toHaveBeenCalledWith({
cfg,
account: "ops" ,
displayName: "Ops Bot" ,
avatarUrl: "mxc://example/avatar",
});
expect(result.details).toMatchObject({
ok: true ,
accountId: "ops" ,
profile: {
displayNameUpdated: true ,
avatarUpdated: true ,
},
});
});
it("accepts local avatar paths for self-profile updates" , async () => {
const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig;
await handleMatrixAction(
{
action: "setProfile" ,
accountId: "ops" ,
path: "/tmp/avatar.jpg" ,
},
cfg,
);
expect(mocks.applyMatrixProfileUpdate).toHaveBeenCalledWith({
cfg,
account: "ops" ,
displayName: undefined,
avatarUrl: undefined,
avatarPath: "/tmp/avatar.jpg" ,
});
});
it("respects account-scoped action overrides when gating direct tool actions" , async () => {
await expect(
handleMatrixAction(
{
action: "sendMessage" ,
accountId: "ops" ,
to: "room:!room:example" ,
content: "hello" ,
},
{
channels: {
matrix: {
actions: {
messages: true ,
},
accounts: {
ops: {
actions: {
messages: false ,
},
},
},
},
},
} as CoreConfig,
),
).rejects.toThrow("Matrix messages are disabled." );
});
});
Messung V0.5 in Prozent C=94 H=96 G=94
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland