From 3425823dfb52b8b5b68fda62816c7c2a6365c33c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 14 Apr 2026 16:18:40 -0400 Subject: [PATCH] fix(regression): avoid sync startup for matrix status reads --- .../matrix/src/matrix/actions/devices.test.ts | 48 ++++++++- .../matrix/src/matrix/actions/devices.ts | 6 +- .../src/matrix/actions/verification.test.ts | 101 +++++++++++++++++- .../matrix/src/matrix/actions/verification.ts | 8 +- 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/extensions/matrix/src/matrix/actions/devices.test.ts b/extensions/matrix/src/matrix/actions/devices.test.ts index 17bf92e176d..0892c811ad2 100644 --- a/extensions/matrix/src/matrix/actions/devices.test.ts +++ b/extensions/matrix/src/matrix/actions/devices.test.ts @@ -1,20 +1,23 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +const withResolvedActionClientMock = vi.fn(); const withStartedActionClientMock = vi.fn(); vi.mock("./client.js", () => ({ + withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args), withStartedActionClient: (...args: unknown[]) => withStartedActionClientMock(...args), })); -const { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } = await import("./devices.js"); +const { getMatrixDeviceHealth, listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } = + await import("./devices.js"); describe("matrix device actions", () => { beforeEach(() => { vi.clearAllMocks(); }); - it("lists own devices on a started client", async () => { - withStartedActionClientMock.mockImplementation(async (_opts, run) => { + it("lists own devices without starting a sync client", async () => { + withResolvedActionClientMock.mockImplementation(async (_opts, run) => { return await run({ listOwnDevices: vi.fn(async () => [ { @@ -30,10 +33,11 @@ describe("matrix device actions", () => { const result = await listMatrixOwnDevices({ accountId: "poe" }); - expect(withStartedActionClientMock).toHaveBeenCalledWith( + expect(withResolvedActionClientMock).toHaveBeenCalledWith( { accountId: "poe" }, expect.any(Function), ); + expect(withStartedActionClientMock).not.toHaveBeenCalled(); expect(result).toEqual([ expect.objectContaining({ deviceId: "A7hWrQ70ea", @@ -42,6 +46,42 @@ describe("matrix device actions", () => { ]); }); + it("computes device health without starting a sync client", async () => { + withResolvedActionClientMock.mockImplementation(async (_opts, run) => { + return await run({ + listOwnDevices: vi.fn(async () => [ + { + deviceId: "du314Zpw3A", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: true, + }, + { + deviceId: "old123", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + ]), + }); + }); + + const result = await getMatrixDeviceHealth({ accountId: "poe" }); + + expect(result.staleOpenClawDevices).toEqual([ + expect.objectContaining({ + deviceId: "old123", + }), + ]); + expect(withResolvedActionClientMock).toHaveBeenCalledWith( + { accountId: "poe" }, + expect.any(Function), + ); + expect(withStartedActionClientMock).not.toHaveBeenCalled(); + }); + it("prunes stale OpenClaw-managed devices but preserves the current device", async () => { const deleteOwnDevices = vi.fn(async () => ({ currentDeviceId: "du314Zpw3A", diff --git a/extensions/matrix/src/matrix/actions/devices.ts b/extensions/matrix/src/matrix/actions/devices.ts index ab6769cbfb8..27735fc081f 100644 --- a/extensions/matrix/src/matrix/actions/devices.ts +++ b/extensions/matrix/src/matrix/actions/devices.ts @@ -1,9 +1,9 @@ import { summarizeMatrixDeviceHealth } from "../device-health.js"; -import { withStartedActionClient } from "./client.js"; +import { withResolvedActionClient, withStartedActionClient } from "./client.js"; import type { MatrixActionClientOpts } from "./types.js"; export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) { - return await withStartedActionClient(opts, async (client) => await client.listOwnDevices()); + return await withResolvedActionClient(opts, async (client) => await client.listOwnDevices()); } export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpts = {}) { @@ -28,7 +28,7 @@ export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpt } export async function getMatrixDeviceHealth(opts: MatrixActionClientOpts = {}) { - return await withStartedActionClient(opts, async (client) => + return await withResolvedActionClient(opts, async (client) => summarizeMatrixDeviceHealth(await client.listOwnDevices()), ); } diff --git a/extensions/matrix/src/matrix/actions/verification.test.ts b/extensions/matrix/src/matrix/actions/verification.test.ts index f3f9dbc880c..2c6242daa21 100644 --- a/extensions/matrix/src/matrix/actions/verification.test.ts +++ b/extensions/matrix/src/matrix/actions/verification.test.ts @@ -1,5 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +const withResolvedActionClientMock = vi.fn(); const withStartedActionClientMock = vi.fn(); const loadConfigMock = vi.fn(() => ({ channels: { @@ -16,14 +17,23 @@ vi.mock("../../runtime.js", () => ({ })); vi.mock("./client.js", () => ({ + withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args), withStartedActionClient: (...args: unknown[]) => withStartedActionClientMock(...args), })); let listMatrixVerifications: typeof import("./verification.js").listMatrixVerifications; +let getMatrixEncryptionStatus: typeof import("./verification.js").getMatrixEncryptionStatus; +let getMatrixRoomKeyBackupStatus: typeof import("./verification.js").getMatrixRoomKeyBackupStatus; +let getMatrixVerificationStatus: typeof import("./verification.js").getMatrixVerificationStatus; describe("matrix verification actions", () => { beforeAll(async () => { - ({ listMatrixVerifications } = await import("./verification.js")); + ({ + getMatrixEncryptionStatus, + getMatrixRoomKeyBackupStatus, + getMatrixVerificationStatus, + listMatrixVerifications, + } = await import("./verification.js")); }); beforeEach(() => { @@ -102,4 +112,93 @@ describe("matrix verification actions", () => { ); expect(loadConfigMock).not.toHaveBeenCalled(); }); + + it("resolves verification status without starting the Matrix client", async () => { + withResolvedActionClientMock.mockImplementation(async (_opts, run) => { + return await run({ + crypto: { + listVerifications: vi.fn(async () => []), + getRecoveryKey: vi.fn(async () => ({ + encodedPrivateKey: "rec-key", + })), + }, + getOwnDeviceVerificationStatus: vi.fn(async () => ({ + encryptionEnabled: true, + verified: true, + userId: "@bot:example.org", + deviceId: "DEVICE123", + localVerified: true, + crossSigningVerified: true, + signedByOwner: true, + recoveryKeyStored: true, + recoveryKeyCreatedAt: null, + recoveryKeyId: "SSSS", + backupVersion: "11", + backup: { + serverVersion: "11", + activeVersion: "11", + trusted: true, + matchesDecryptionKey: true, + decryptionKeyCached: true, + keyLoadAttempted: false, + keyLoadError: null, + }, + })), + }); + }); + + const status = await getMatrixVerificationStatus({ includeRecoveryKey: true }); + + expect(status).toMatchObject({ + verified: true, + pendingVerifications: 0, + recoveryKey: "rec-key", + }); + expect(withResolvedActionClientMock).toHaveBeenCalledTimes(1); + expect(withStartedActionClientMock).not.toHaveBeenCalled(); + }); + + it("resolves encryption and backup status without starting the Matrix client", async () => { + withResolvedActionClientMock + .mockImplementationOnce(async (_opts, run) => { + return await run({ + crypto: { + getRecoveryKey: vi.fn(async () => ({ + encodedPrivateKey: "rec-key", + createdAt: "2026-01-01T00:00:00.000Z", + })), + listVerifications: vi.fn(async () => [{ id: "req-1" }]), + }, + }); + }) + .mockImplementationOnce(async (_opts, run) => { + return await run({ + getRoomKeyBackupStatus: vi.fn(async () => ({ + serverVersion: "11", + activeVersion: "11", + trusted: true, + matchesDecryptionKey: true, + decryptionKeyCached: true, + keyLoadAttempted: false, + keyLoadError: null, + })), + }); + }); + + const encryption = await getMatrixEncryptionStatus({ includeRecoveryKey: true }); + const backup = await getMatrixRoomKeyBackupStatus(); + + expect(encryption).toMatchObject({ + encryptionEnabled: true, + recoveryKeyStored: true, + recoveryKey: "rec-key", + pendingVerifications: 1, + }); + expect(backup).toMatchObject({ + serverVersion: "11", + trusted: true, + }); + expect(withResolvedActionClientMock).toHaveBeenCalledTimes(2); + expect(withStartedActionClientMock).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/matrix/src/matrix/actions/verification.ts b/extensions/matrix/src/matrix/actions/verification.ts index ffef5364a44..cdb4a8dac07 100644 --- a/extensions/matrix/src/matrix/actions/verification.ts +++ b/extensions/matrix/src/matrix/actions/verification.ts @@ -2,7 +2,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { getMatrixRuntime } from "../../runtime.js"; import type { CoreConfig } from "../../types.js"; import { formatMatrixEncryptionUnavailableError } from "../encryption-guidance.js"; -import { withStartedActionClient } from "./client.js"; +import { withResolvedActionClient, withStartedActionClient } from "./client.js"; import type { MatrixActionClientOpts } from "./types.js"; function requireCrypto( @@ -152,7 +152,7 @@ export async function confirmMatrixVerificationReciprocateQr( export async function getMatrixEncryptionStatus( opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, ) { - return await withStartedActionClient(opts, async (client) => { + return await withResolvedActionClient(opts, async (client) => { const crypto = requireCrypto(client, opts); const recoveryKey = await crypto.getRecoveryKey(); return { @@ -168,7 +168,7 @@ export async function getMatrixEncryptionStatus( export async function getMatrixVerificationStatus( opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, ) { - return await withStartedActionClient(opts, async (client) => { + return await withResolvedActionClient(opts, async (client) => { const status = await client.getOwnDeviceVerificationStatus(); const payload = { ...status, @@ -186,7 +186,7 @@ export async function getMatrixVerificationStatus( } export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) { - return await withStartedActionClient( + return await withResolvedActionClient( opts, async (client) => await client.getRoomKeyBackupStatus(), );