fix(regression): avoid sync startup for matrix status reads

This commit is contained in:
Gustavo Madeira Santana 2026-04-14 16:18:40 -04:00
parent c96871db30
commit 3425823dfb
No known key found for this signature in database
4 changed files with 151 additions and 12 deletions

View file

@ -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",

View file

@ -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()),
);
}

View file

@ -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();
});
});

View file

@ -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(),
);