diff --git a/extensions/matrix/src/matrix/actions/limits.test.ts b/extensions/matrix/src/matrix/actions/limits.test.ts new file mode 100644 index 0000000000..d6c85ab7fa --- /dev/null +++ b/extensions/matrix/src/matrix/actions/limits.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { resolveMatrixActionLimit } from "./limits.js"; + +describe("resolveMatrixActionLimit", () => { + it("uses fallback for non-finite values", () => { + expect(resolveMatrixActionLimit(undefined, 20)).toBe(20); + expect(resolveMatrixActionLimit(Number.NaN, 20)).toBe(20); + }); + + it("normalizes finite numbers to positive integers", () => { + expect(resolveMatrixActionLimit(7.9, 20)).toBe(7); + expect(resolveMatrixActionLimit(0, 20)).toBe(1); + expect(resolveMatrixActionLimit(-3, 20)).toBe(1); + }); +}); diff --git a/extensions/matrix/src/matrix/actions/pins.test.ts b/extensions/matrix/src/matrix/actions/pins.test.ts new file mode 100644 index 0000000000..2b432c1a85 --- /dev/null +++ b/extensions/matrix/src/matrix/actions/pins.test.ts @@ -0,0 +1,74 @@ +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; +import { describe, expect, it, vi } from "vitest"; +import { listMatrixPins, pinMatrixMessage, unpinMatrixMessage } from "./pins.js"; + +function createPinsClient(seedPinned: string[], knownBodies: Record = {}) { + let pinned = [...seedPinned]; + const getRoomStateEvent = vi.fn(async () => ({ pinned: [...pinned] })); + const sendStateEvent = vi.fn( + async (_roomId: string, _type: string, _key: string, payload: any) => { + pinned = [...payload.pinned]; + }, + ); + const getEvent = vi.fn(async (_roomId: string, eventId: string) => { + const body = knownBodies[eventId]; + if (!body) { + throw new Error("missing"); + } + return { + event_id: eventId, + sender: "@alice:example.org", + type: "m.room.message", + origin_server_ts: 123, + content: { msgtype: "m.text", body }, + }; + }); + + return { + client: { + getRoomStateEvent, + sendStateEvent, + getEvent, + stop: vi.fn(), + } as unknown as MatrixClient, + getPinned: () => pinned, + sendStateEvent, + }; +} + +describe("matrix pins actions", () => { + it("pins a message once even when asked twice", async () => { + const { client, getPinned, sendStateEvent } = createPinsClient(["$a"]); + + const first = await pinMatrixMessage("!room:example.org", "$b", { client }); + const second = await pinMatrixMessage("!room:example.org", "$b", { client }); + + expect(first.pinned).toEqual(["$a", "$b"]); + expect(second.pinned).toEqual(["$a", "$b"]); + expect(getPinned()).toEqual(["$a", "$b"]); + expect(sendStateEvent).toHaveBeenCalledTimes(2); + }); + + it("unpinds only the selected message id", async () => { + const { client, getPinned } = createPinsClient(["$a", "$b", "$c"]); + + const result = await unpinMatrixMessage("!room:example.org", "$b", { client }); + + expect(result.pinned).toEqual(["$a", "$c"]); + expect(getPinned()).toEqual(["$a", "$c"]); + }); + + it("lists pinned ids and summarizes only resolvable events", async () => { + const { client } = createPinsClient(["$a", "$missing"], { $a: "hello" }); + + const result = await listMatrixPins("!room:example.org", { client }); + + expect(result.pinned).toEqual(["$a", "$missing"]); + expect(result.events).toEqual([ + expect.objectContaining({ + eventId: "$a", + body: "hello", + }), + ]); + }); +}); diff --git a/extensions/matrix/src/matrix/actions/reactions.test.ts b/extensions/matrix/src/matrix/actions/reactions.test.ts new file mode 100644 index 0000000000..aab161b54c --- /dev/null +++ b/extensions/matrix/src/matrix/actions/reactions.test.ts @@ -0,0 +1,109 @@ +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; +import { describe, expect, it, vi } from "vitest"; +import { listMatrixReactions, removeMatrixReactions } from "./reactions.js"; + +function createReactionsClient(params: { + chunk: Array<{ + event_id?: string; + sender?: string; + key?: string; + }>; + userId?: string | null; +}) { + const doRequest = vi.fn(async (_method: string, _path: string, _query: any) => ({ + chunk: params.chunk.map((item) => ({ + event_id: item.event_id ?? "", + sender: item.sender ?? "", + content: item.key + ? { + "m.relates_to": { + rel_type: "m.annotation", + event_id: "$target", + key: item.key, + }, + } + : {}, + })), + })); + const getUserId = vi.fn(async () => params.userId ?? null); + const redactEvent = vi.fn(async () => undefined); + + return { + client: { + doRequest, + getUserId, + redactEvent, + stop: vi.fn(), + } as unknown as MatrixClient, + doRequest, + redactEvent, + }; +} + +describe("matrix reaction actions", () => { + it("aggregates reactions by key and unique sender", async () => { + const { client, doRequest } = createReactionsClient({ + chunk: [ + { event_id: "$1", sender: "@alice:example.org", key: "👍" }, + { event_id: "$2", sender: "@bob:example.org", key: "👍" }, + { event_id: "$3", sender: "@alice:example.org", key: "👎" }, + { event_id: "$4", sender: "@bot:example.org" }, + ], + userId: "@bot:example.org", + }); + + const result = await listMatrixReactions("!room:example.org", "$msg", { client, limit: 2.9 }); + + expect(doRequest).toHaveBeenCalledWith( + "GET", + expect.stringContaining("/rooms/!room%3Aexample.org/relations/%24msg/"), + expect.objectContaining({ limit: 2 }), + ); + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: "👍", + count: 2, + users: expect.arrayContaining(["@alice:example.org", "@bob:example.org"]), + }), + expect.objectContaining({ + key: "👎", + count: 1, + users: ["@alice:example.org"], + }), + ]), + ); + }); + + it("removes only current-user reactions matching emoji filter", async () => { + const { client, redactEvent } = createReactionsClient({ + chunk: [ + { event_id: "$1", sender: "@me:example.org", key: "👍" }, + { event_id: "$2", sender: "@me:example.org", key: "👎" }, + { event_id: "$3", sender: "@other:example.org", key: "👍" }, + ], + userId: "@me:example.org", + }); + + const result = await removeMatrixReactions("!room:example.org", "$msg", { + client, + emoji: "👍", + }); + + expect(result).toEqual({ removed: 1 }); + expect(redactEvent).toHaveBeenCalledTimes(1); + expect(redactEvent).toHaveBeenCalledWith("!room:example.org", "$1"); + }); + + it("returns removed=0 when current user id is unavailable", async () => { + const { client, redactEvent } = createReactionsClient({ + chunk: [{ event_id: "$1", sender: "@me:example.org", key: "👍" }], + userId: null, + }); + + const result = await removeMatrixReactions("!room:example.org", "$msg", { client }); + + expect(result).toEqual({ removed: 0 }); + expect(redactEvent).not.toHaveBeenCalled(); + }); +});