-
-
Notifications
You must be signed in to change notification settings - Fork 69.5k
fix(matrix): fix multiple Conduit compatibility issues preventing message delivery #31023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
677b565
ac310e9
810cb9d
220549f
e6333bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { resolveSharedMatrixClient, stopSharedClient } from "./shared.js"; | ||
| import type { MatrixAuth } from "./types.js"; | ||
|
|
||
| const createMatrixClientMock = vi.hoisted(() => vi.fn()); | ||
|
|
||
| vi.mock("./create-client.js", () => ({ | ||
| createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args), | ||
| })); | ||
|
|
||
| function makeAuth(suffix: string): MatrixAuth { | ||
| return { | ||
| homeserver: "https://matrix.example.org", | ||
| userId: `@bot-${suffix}:example.org`, | ||
| accessToken: `token-${suffix}`, | ||
| encryption: false, | ||
| }; | ||
| } | ||
|
|
||
| function createMockClient(startImpl: () => Promise<void>): MatrixClient { | ||
| return { | ||
| start: vi.fn(startImpl), | ||
| stop: vi.fn(), | ||
| getJoinedRooms: vi.fn().mockResolvedValue([]), | ||
| crypto: undefined, | ||
| } as unknown as MatrixClient; | ||
| } | ||
|
|
||
| describe("resolveSharedMatrixClient startup behavior", () => { | ||
| afterEach(() => { | ||
| stopSharedClient(); | ||
| createMatrixClientMock.mockReset(); | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| it("propagates the original start error during initialization", async () => { | ||
| vi.useFakeTimers(); | ||
| const startError = new Error("bad token"); | ||
| const client = createMockClient( | ||
| () => | ||
| new Promise<void>((_resolve, reject) => { | ||
| setTimeout(() => reject(startError), 1); | ||
| }), | ||
| ); | ||
| createMatrixClientMock.mockResolvedValue(client); | ||
|
|
||
| const startPromise = resolveSharedMatrixClient({ | ||
| auth: makeAuth("start-error"), | ||
| }); | ||
| const startExpectation = expect(startPromise).rejects.toBe(startError); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(2001); | ||
| await startExpectation; | ||
| }); | ||
|
|
||
| it("retries start after a late start-loop failure", async () => { | ||
| vi.useFakeTimers(); | ||
| let rejectFirstStart: ((err: unknown) => void) | undefined; | ||
| const firstStart = new Promise<void>((_resolve, reject) => { | ||
| rejectFirstStart = reject; | ||
| }); | ||
| const secondStart = new Promise<void>(() => {}); | ||
| const startMock = vi.fn().mockReturnValueOnce(firstStart).mockReturnValueOnce(secondStart); | ||
| const client = createMockClient(startMock); | ||
| createMatrixClientMock.mockResolvedValue(client); | ||
|
|
||
| const firstResolve = resolveSharedMatrixClient({ | ||
| auth: makeAuth("late-failure"), | ||
| }); | ||
| await vi.advanceTimersByTimeAsync(2000); | ||
| await expect(firstResolve).resolves.toBe(client); | ||
| expect(startMock).toHaveBeenCalledTimes(1); | ||
|
|
||
| rejectFirstStart?.(new Error("late failure")); | ||
| await Promise.resolve(); | ||
|
|
||
| const secondResolve = resolveSharedMatrixClient({ | ||
| auth: makeAuth("late-failure"), | ||
| }); | ||
| await vi.advanceTimersByTimeAsync(2000); | ||
| await expect(secondResolve).resolves.toBe(client); | ||
| expect(startMock).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { createDirectRoomTracker } from "./direct.js"; | ||
|
|
||
| function createMockClient(params: { | ||
| isDm?: boolean; | ||
| senderDirect?: boolean; | ||
| selfDirect?: boolean; | ||
| members?: string[]; | ||
| }) { | ||
| const members = params.members ?? ["@alice:example.org", "@bot:example.org"]; | ||
| return { | ||
| dms: { | ||
| update: vi.fn().mockResolvedValue(undefined), | ||
| isDm: vi.fn().mockReturnValue(params.isDm === true), | ||
| }, | ||
| getUserId: vi.fn().mockResolvedValue("@bot:example.org"), | ||
| getJoinedRoomMembers: vi.fn().mockResolvedValue(members), | ||
| getRoomStateEvent: vi | ||
| .fn() | ||
| .mockImplementation(async (_roomId: string, _event: string, stateKey: string) => { | ||
| if (stateKey === "@alice:example.org") { | ||
| return { is_direct: params.senderDirect === true }; | ||
| } | ||
| if (stateKey === "@bot:example.org") { | ||
| return { is_direct: params.selfDirect === true }; | ||
| } | ||
| return {}; | ||
| }), | ||
| } as unknown as MatrixClient; | ||
| } | ||
|
|
||
| describe("createDirectRoomTracker", () => { | ||
| it("treats m.direct rooms as DMs", async () => { | ||
| const tracker = createDirectRoomTracker(createMockClient({ isDm: true })); | ||
| await expect( | ||
| tracker.isDirectMessage({ | ||
| roomId: "!room:example.org", | ||
| senderId: "@alice:example.org", | ||
| }), | ||
| ).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it("does not classify 2-member rooms as DMs without direct flags", async () => { | ||
| const tracker = createDirectRoomTracker(createMockClient({ isDm: false })); | ||
| await expect( | ||
| tracker.isDirectMessage({ | ||
| roomId: "!room:example.org", | ||
| senderId: "@alice:example.org", | ||
| }), | ||
| ).resolves.toBe(false); | ||
| }); | ||
|
|
||
| it("uses is_direct member flags when present", async () => { | ||
| const tracker = createDirectRoomTracker(createMockClient({ senderDirect: true })); | ||
| await expect( | ||
| tracker.isDirectMessage({ | ||
| roomId: "!room:example.org", | ||
| senderId: "@alice:example.org", | ||
| }), | ||
| ).resolves.toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,17 +78,13 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr | |
| const { roomId, senderId } = params; | ||
| await refreshDmCache(); | ||
|
|
||
| // Check m.direct account data (most authoritative) | ||
| if (client.dms.isDm(roomId)) { | ||
| log(`matrix: dm detected via m.direct room=${roomId}`); | ||
| return true; | ||
| } | ||
|
|
||
| const memberCount = await resolveMemberCount(roomId); | ||
| if (memberCount === 2) { | ||
| log(`matrix: dm detected via member count room=${roomId} members=${memberCount}`); | ||
| return true; | ||
| } | ||
|
|
||
| // Check m.room.member state for is_direct flag | ||
| const selfUserId = params.selfUserId ?? (await ensureSelfUserId()); | ||
| const directViaState = | ||
| (await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? "")); | ||
|
|
@@ -97,6 +93,12 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr | |
| return true; | ||
| } | ||
|
|
||
| // Member count alone is NOT a reliable DM indicator. | ||
| // Explicitly configured group rooms with 2 members (e.g. bot + one user) | ||
| // were being misclassified as DMs, causing messages to be routed through | ||
| // DM policy instead of group policy and silently dropped. | ||
| // See: https://github.com/openclaw/openclaw/issues/20145 | ||
| const memberCount = await resolveMemberCount(roomId); | ||
| log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`); | ||
| return false; | ||
|
Comment on lines
+101
to
103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This change now returns Useful? React with πΒ / π. |
||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { DEFAULT_STARTUP_GRACE_MS, isConfiguredMatrixRoomEntry } from "./index.js"; | ||
|
|
||
| describe("monitorMatrixProvider helpers", () => { | ||
| it("treats !-prefixed room IDs as configured room entries", () => { | ||
| expect(isConfiguredMatrixRoomEntry("!abc123")).toBe(true); | ||
| expect(isConfiguredMatrixRoomEntry("!RoomMixedCase")).toBe(true); | ||
| }); | ||
|
|
||
| it("requires a homeserver suffix for # aliases", () => { | ||
| expect(isConfiguredMatrixRoomEntry("#alias:example.org")).toBe(true); | ||
| expect(isConfiguredMatrixRoomEntry("#alias")).toBe(false); | ||
| }); | ||
|
|
||
| it("uses a non-zero startup grace window", () => { | ||
| expect(DEFAULT_STARTUP_GRACE_MS).toBe(5000); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was added back by 94693f7#diff-7d63f9d768d8a8e34c19d3fd4e56509d1960230c24b7fd06bb048d56ee3e354dR31