mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
336 lines
9.1 KiB
TypeScript
336 lines
9.1 KiB
TypeScript
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", async () => {
|
|
const actual = await vi.importActual<typeof import("./matrix/actions.js")>("./matrix/actions.js");
|
|
return {
|
|
...actual,
|
|
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", async () => {
|
|
const actual = await vi.importActual<typeof import("./matrix/send.js")>("./matrix/send.js");
|
|
return {
|
|
...actual,
|
|
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("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,
|
|
);
|
|
|
|
expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example", "hello", {
|
|
cfg,
|
|
accountId: "ops",
|
|
mediaUrl: undefined,
|
|
replyToId: undefined,
|
|
threadId: "$thread",
|
|
});
|
|
});
|
|
|
|
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.");
|
|
});
|
|
});
|