From 238ff2def98bdbf9691db1b241909a9d506a1b49 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 11 Mar 2026 21:34:11 +0000 Subject: [PATCH] fMatrix: fix remaining typecheck regressions --- .../matrix/client-resolver.test-helpers.ts | 16 +- .../matrix/src/matrix/monitor/direct.ts | 4 +- .../matrix/monitor/handler.test-helpers.ts | 21 ++- .../matrix/src/matrix/monitor/handler.test.ts | 2 +- .../matrix/src/matrix/monitor/startup.test.ts | 155 ++++++++++++------ .../matrix/src/matrix/monitor/startup.ts | 1 + extensions/matrix/src/matrix/sdk.ts | 24 +-- .../src/matrix/sdk/recovery-key-store.ts | 6 +- .../matrix/src/matrix/send/client.test.ts | 2 +- extensions/matrix/src/onboarding.test.ts | 33 ++++ extensions/matrix/src/onboarding.ts | 5 +- src/infra/matrix-legacy-crypto.test.ts | 23 +++ src/infra/matrix-legacy-crypto.ts | 75 +++++++-- 13 files changed, 281 insertions(+), 86 deletions(-) diff --git a/extensions/matrix/src/matrix/client-resolver.test-helpers.ts b/extensions/matrix/src/matrix/client-resolver.test-helpers.ts index 99e9d2f39f0..e13fce252d0 100644 --- a/extensions/matrix/src/matrix/client-resolver.test-helpers.ts +++ b/extensions/matrix/src/matrix/client-resolver.test-helpers.ts @@ -1,7 +1,19 @@ -import { vi } from "vitest"; +import { vi, type Mock } from "vitest"; import type { MatrixClient } from "./sdk.js"; -export const matrixClientResolverMocks = { +type MatrixClientResolverMocks = { + loadConfigMock: Mock<() => unknown>; + getMatrixRuntimeMock: Mock<() => unknown>; + getActiveMatrixClientMock: Mock<(...args: unknown[]) => MatrixClient | null>; + createMatrixClientMock: Mock<(...args: unknown[]) => Promise>; + isBunRuntimeMock: Mock<() => boolean>; + resolveMatrixAuthMock: Mock<(...args: unknown[]) => Promise>; + resolveMatrixAuthContextMock: Mock< + (params: { cfg: unknown; accountId?: string | null }) => unknown + >; +}; + +export const matrixClientResolverMocks: MatrixClientResolverMocks = { loadConfigMock: vi.fn(() => ({})), getMatrixRuntimeMock: vi.fn(), getActiveMatrixClientMock: vi.fn(), diff --git a/extensions/matrix/src/matrix/monitor/direct.ts b/extensions/matrix/src/matrix/monitor/direct.ts index 5da4fde768b..a74b2b61975 100644 --- a/extensions/matrix/src/matrix/monitor/direct.ts +++ b/extensions/matrix/src/matrix/monitor/direct.ts @@ -102,7 +102,9 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr const memberCount = await resolveMemberCount(roomId); if (memberCount === 2) { try { - const nameState = await client.getRoomStateEvent(roomId, "m.room.name", ""); + const nameState = (await client.getRoomStateEvent(roomId, "m.room.name", "")) as { + name?: string | null; + } | null; if (!nameState?.name?.trim()) { log(`matrix: dm detected via fallback (2 members, no room name) room=${roomId}`); return true; diff --git a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts index 4996a72ff3e..7a548755e5b 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts @@ -64,7 +64,23 @@ type MatrixHandlerTestHarnessOptions = { getMemberDisplayName?: MatrixMonitorHandlerParams["getMemberDisplayName"]; }; -export function createMatrixHandlerTestHarness(options: MatrixHandlerTestHarnessOptions = {}) { +type MatrixHandlerTestHarness = { + dispatchReplyFromConfig: () => Promise<{ + queuedFinal: boolean; + counts: { final: number; block: number; tool: number }; + }>; + enqueueSystemEvent: (...args: unknown[]) => void; + finalizeInboundContext: (ctx: unknown) => unknown; + handler: ReturnType; + readAllowFromStore: MatrixMonitorHandlerParams["core"]["channel"]["pairing"]["readAllowFromStore"]; + recordInboundSession: (...args: unknown[]) => Promise; + resolveAgentRoute: () => typeof DEFAULT_ROUTE; + upsertPairingRequest: MatrixMonitorHandlerParams["core"]["channel"]["pairing"]["upsertPairingRequest"]; +}; + +export function createMatrixHandlerTestHarness( + options: MatrixHandlerTestHarnessOptions = {}, +): MatrixHandlerTestHarness { const readAllowFromStore = options.readAllowFromStore ?? vi.fn(async () => [] as string[]); const upsertPairingRequest = options.upsertPairingRequest ?? vi.fn(async () => ({ code: "ABCDEFGH", created: false })); @@ -109,7 +125,8 @@ export function createMatrixHandlerTestHarness(options: MatrixHandlerTestHarness }, reply: { resolveEnvelopeFormatOptions: options.resolveEnvelopeFormatOptions ?? (() => ({})), - formatAgentEnvelope: options.formatAgentEnvelope ?? (({ body }) => body), + formatAgentEnvelope: + options.formatAgentEnvelope ?? (({ body }: { body: string }) => body), finalizeInboundContext, createReplyDispatcherWithTyping: options.createReplyDispatcherWithTyping ?? diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 9d1655b3444..cd151b2a16c 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -188,7 +188,7 @@ describe("matrix monitor handler pairing account scope", () => { accountId: "ops", sessionKey: "agent:ops:main", mainSessionKey: "agent:ops:main", - matchedBy: "binding.account", + matchedBy: "binding.account" as const, })); const { handler } = createMatrixHandlerTestHarness({ diff --git a/extensions/matrix/src/matrix/monitor/startup.test.ts b/extensions/matrix/src/matrix/monitor/startup.test.ts index d093dfff121..44d328fb811 100644 --- a/extensions/matrix/src/matrix/monitor/startup.test.ts +++ b/extensions/matrix/src/matrix/monitor/startup.test.ts @@ -1,66 +1,121 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { MatrixProfileSyncResult } from "../profile.js"; +import type { MatrixOwnDeviceVerificationStatus } from "../sdk.js"; +import type { MatrixLegacyCryptoRestoreResult } from "./legacy-crypto-restore.js"; +import type { MatrixStartupVerificationOutcome } from "./startup-verification.js"; import { runMatrixStartupMaintenance } from "./startup.js"; -const hoisted = vi.hoisted(() => ({ - maybeRestoreLegacyMatrixBackup: vi.fn(async () => ({ kind: "skipped" as const })), - summarizeMatrixDeviceHealth: vi.fn(() => ({ - staleOpenClawDevices: [] as Array<{ deviceId: string }>, - })), - syncMatrixOwnProfile: vi.fn(async () => ({ +function createVerificationStatus( + overrides: Partial = {}, +): MatrixOwnDeviceVerificationStatus { + return { + encryptionEnabled: true, + userId: "@bot:example.org", + deviceId: "DEVICE", + verified: false, + localVerified: false, + crossSigningVerified: false, + signedByOwner: false, + recoveryKeyStored: false, + recoveryKeyCreatedAt: null, + recoveryKeyId: null, + backupVersion: null, + backup: { + serverVersion: null, + activeVersion: null, + trusted: null, + matchesDecryptionKey: null, + decryptionKeyCached: null, + keyLoadAttempted: false, + keyLoadError: null, + }, + ...overrides, + }; +} + +function createProfileSyncResult( + overrides: Partial = {}, +): MatrixProfileSyncResult { + return { skipped: false, displayNameUpdated: false, avatarUpdated: false, resolvedAvatarUrl: null, uploadedAvatarSource: null, convertedAvatarFromHttp: false, + ...overrides, + }; +} + +function createStartupVerificationOutcome( + kind: Exclude, + overrides: Partial> = {}, +): MatrixStartupVerificationOutcome { + return { + kind, + verification: createVerificationStatus({ verified: kind === "verified" }), + ...overrides, + } as MatrixStartupVerificationOutcome; +} + +function createLegacyCryptoRestoreResult( + overrides: Partial = {}, +): MatrixLegacyCryptoRestoreResult { + return { + kind: "skipped", + ...overrides, + } as MatrixLegacyCryptoRestoreResult; +} + +const hoisted = vi.hoisted(() => ({ + maybeRestoreLegacyMatrixBackup: vi.fn(async () => createLegacyCryptoRestoreResult()), + summarizeMatrixDeviceHealth: vi.fn(() => ({ + staleOpenClawDevices: [] as Array<{ deviceId: string }>, })), - ensureMatrixStartupVerification: vi.fn(async () => ({ kind: "verified" as const })), + syncMatrixOwnProfile: vi.fn(async () => createProfileSyncResult()), + ensureMatrixStartupVerification: vi.fn(async () => createStartupVerificationOutcome("verified")), updateMatrixAccountConfig: vi.fn((cfg: unknown) => cfg), })); vi.mock("../config-update.js", () => ({ - updateMatrixAccountConfig: (...args: unknown[]) => hoisted.updateMatrixAccountConfig(...args), + updateMatrixAccountConfig: hoisted.updateMatrixAccountConfig, })); vi.mock("../device-health.js", () => ({ - summarizeMatrixDeviceHealth: (...args: unknown[]) => hoisted.summarizeMatrixDeviceHealth(...args), + summarizeMatrixDeviceHealth: hoisted.summarizeMatrixDeviceHealth, })); vi.mock("../profile.js", () => ({ - syncMatrixOwnProfile: (...args: unknown[]) => hoisted.syncMatrixOwnProfile(...args), + syncMatrixOwnProfile: hoisted.syncMatrixOwnProfile, })); vi.mock("./legacy-crypto-restore.js", () => ({ - maybeRestoreLegacyMatrixBackup: (...args: unknown[]) => - hoisted.maybeRestoreLegacyMatrixBackup(...args), + maybeRestoreLegacyMatrixBackup: hoisted.maybeRestoreLegacyMatrixBackup, })); vi.mock("./startup-verification.js", () => ({ - ensureMatrixStartupVerification: (...args: unknown[]) => - hoisted.ensureMatrixStartupVerification(...args), + ensureMatrixStartupVerification: hoisted.ensureMatrixStartupVerification, })); describe("runMatrixStartupMaintenance", () => { beforeEach(() => { - hoisted.maybeRestoreLegacyMatrixBackup.mockClear().mockResolvedValue({ kind: "skipped" }); + hoisted.maybeRestoreLegacyMatrixBackup + .mockClear() + .mockResolvedValue(createLegacyCryptoRestoreResult()); hoisted.summarizeMatrixDeviceHealth.mockClear().mockReturnValue({ staleOpenClawDevices: [] }); - hoisted.syncMatrixOwnProfile.mockClear().mockResolvedValue({ - skipped: false, - displayNameUpdated: false, - avatarUpdated: false, - resolvedAvatarUrl: null, - uploadedAvatarSource: null, - convertedAvatarFromHttp: false, - }); - hoisted.ensureMatrixStartupVerification.mockClear().mockResolvedValue({ kind: "verified" }); + hoisted.syncMatrixOwnProfile.mockClear().mockResolvedValue(createProfileSyncResult()); + hoisted.ensureMatrixStartupVerification + .mockClear() + .mockResolvedValue(createStartupVerificationOutcome("verified")); hoisted.updateMatrixAccountConfig.mockClear().mockImplementation((cfg: unknown) => cfg); }); - function createParams() { + function createParams(): Parameters[0] { return { client: { crypto: {}, listOwnDevices: vi.fn(async () => []), + getOwnDeviceVerificationStatus: vi.fn(async () => createVerificationStatus()), } as never, auth: { accountId: "ops", @@ -90,20 +145,20 @@ describe("runMatrixStartupMaintenance", () => { fileName: "avatar.png", })), env: {}, - } as const; + }; } it("persists converted avatar URLs after profile sync", async () => { const params = createParams(); const updatedCfg = { channels: { matrix: { avatarUrl: "mxc://avatar" } } }; - hoisted.syncMatrixOwnProfile.mockResolvedValue({ - skipped: false, - displayNameUpdated: false, - avatarUpdated: true, - resolvedAvatarUrl: "mxc://avatar", - uploadedAvatarSource: "http", - convertedAvatarFromHttp: true, - }); + hoisted.syncMatrixOwnProfile.mockResolvedValue( + createProfileSyncResult({ + avatarUpdated: true, + resolvedAvatarUrl: "mxc://avatar", + uploadedAvatarSource: "http", + convertedAvatarFromHttp: true, + }), + ); hoisted.updateMatrixAccountConfig.mockReturnValue(updatedCfg); await runMatrixStartupMaintenance(params); @@ -132,13 +187,17 @@ describe("runMatrixStartupMaintenance", () => { hoisted.summarizeMatrixDeviceHealth.mockReturnValue({ staleOpenClawDevices: [{ deviceId: "DEV123" }], }); - hoisted.ensureMatrixStartupVerification.mockResolvedValue({ kind: "pending" }); - hoisted.maybeRestoreLegacyMatrixBackup.mockResolvedValue({ - kind: "restored", - imported: 2, - total: 3, - localOnlyKeys: 1, - }); + hoisted.ensureMatrixStartupVerification.mockResolvedValue( + createStartupVerificationOutcome("pending"), + ); + hoisted.maybeRestoreLegacyMatrixBackup.mockResolvedValue( + createLegacyCryptoRestoreResult({ + kind: "restored", + imported: 2, + total: 3, + localOnlyKeys: 1, + }), + ); await runMatrixStartupMaintenance(params); @@ -162,10 +221,9 @@ describe("runMatrixStartupMaintenance", () => { it("logs cooldown and request-failure verification outcomes without throwing", async () => { const params = createParams(); params.auth.encryption = true; - hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce({ - kind: "cooldown", - retryAfterMs: 321, - }); + hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce( + createStartupVerificationOutcome("cooldown", { retryAfterMs: 321 }), + ); await runMatrixStartupMaintenance(params); @@ -173,10 +231,9 @@ describe("runMatrixStartupMaintenance", () => { "matrix: skipped startup verification request due to cooldown (retryAfterMs=321)", ); - hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce({ - kind: "request-failed", - error: "boom", - }); + hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce( + createStartupVerificationOutcome("request-failed", { error: "boom" }), + ); await runMatrixStartupMaintenance(params); diff --git a/extensions/matrix/src/matrix/monitor/startup.ts b/extensions/matrix/src/matrix/monitor/startup.ts index 7aec135c3b1..243afa612dd 100644 --- a/extensions/matrix/src/matrix/monitor/startup.ts +++ b/extensions/matrix/src/matrix/monitor/startup.ts @@ -11,6 +11,7 @@ import { ensureMatrixStartupVerification } from "./startup-verification.js"; type MatrixStartupClient = Pick< MatrixClient, | "crypto" + | "getOwnDeviceVerificationStatus" | "getUserProfile" | "listOwnDevices" | "restoreRoomKeyBackup" diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 160ab488961..173bcdcf4b6 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -801,9 +801,9 @@ export class MatrixClient { } let defaultKeyId: string | null | undefined = undefined; - const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret - if (canReadSecretStorageStatus) { - const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret + const getSecretStorageStatus = crypto.getSecretStorageStatus; // pragma: allowlist secret + if (typeof getSecretStorageStatus === "function") { + const status = await getSecretStorageStatus.call(crypto).catch(() => null); // pragma: allowlist secret defaultKeyId = status?.defaultKeyId; } @@ -870,9 +870,9 @@ export class MatrixClient { const rawRecoveryKey = params.recoveryKey?.trim(); if (rawRecoveryKey) { let defaultKeyId: string | null | undefined = undefined; - const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret - if (canReadSecretStorageStatus) { - const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret + const getSecretStorageStatus = crypto.getSecretStorageStatus; // pragma: allowlist secret + if (typeof getSecretStorageStatus === "function") { + const status = await getSecretStorageStatus.call(crypto).catch(() => null); // pragma: allowlist secret defaultKeyId = status?.defaultKeyId; } this.recoveryKeyStore.storeEncodedRecoveryKey({ @@ -1077,9 +1077,9 @@ export class MatrixClient { const rawRecoveryKey = params?.recoveryKey?.trim(); if (rawRecoveryKey) { let defaultKeyId: string | null | undefined = undefined; - const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret - if (canReadSecretStorageStatus) { - const status = await crypto.getSecretStorageStatus().catch(() => null); // pragma: allowlist secret + const getSecretStorageStatus = crypto.getSecretStorageStatus; // pragma: allowlist secret + if (typeof getSecretStorageStatus === "function") { + const status = await getSecretStorageStatus.call(crypto).catch(() => null); // pragma: allowlist secret defaultKeyId = status?.defaultKeyId; } this.recoveryKeyStore.storeEncodedRecoveryKey({ @@ -1190,11 +1190,11 @@ export class MatrixClient { private async resolveCachedRoomKeyBackupDecryptionKey( crypto: MatrixCryptoBootstrapApi, ): Promise { - const canGetSessionBackupPrivateKey = typeof crypto.getSessionBackupPrivateKey === "function"; // pragma: allowlist secret - if (!canGetSessionBackupPrivateKey) { + const getSessionBackupPrivateKey = crypto.getSessionBackupPrivateKey; // pragma: allowlist secret + if (typeof getSessionBackupPrivateKey !== "function") { return null; } - const key = await crypto.getSessionBackupPrivateKey().catch(() => null); // pragma: allowlist secret + const key = await getSessionBackupPrivateKey.call(crypto).catch(() => null); // pragma: allowlist secret return key ? key.length > 0 : false; } diff --git a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts index 564adb1c0a4..7e88df923c7 100644 --- a/extensions/matrix/src/matrix/sdk/recovery-key-store.ts +++ b/extensions/matrix/src/matrix/sdk/recovery-key-store.ts @@ -152,10 +152,10 @@ export class MatrixRecoveryKeyStore { } = {}, ): Promise { let status: MatrixSecretStorageStatus | null = null; - const canReadSecretStorageStatus = typeof crypto.getSecretStorageStatus === "function"; // pragma: allowlist secret - if (canReadSecretStorageStatus) { + const getSecretStorageStatus = crypto.getSecretStorageStatus; // pragma: allowlist secret + if (typeof getSecretStorageStatus === "function") { try { - status = await crypto.getSecretStorageStatus(); + status = await getSecretStorageStatus.call(crypto); } catch (err) { LogService.warn("MatrixClientLite", "Failed to read secret storage status:", err); } diff --git a/extensions/matrix/src/matrix/send/client.test.ts b/extensions/matrix/src/matrix/send/client.test.ts index 4cc3d09b35b..f82fe60ba66 100644 --- a/extensions/matrix/src/matrix/send/client.test.ts +++ b/extensions/matrix/src/matrix/send/client.test.ts @@ -22,7 +22,7 @@ vi.mock("../client.js", () => ({ createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args), isBunRuntime: () => isBunRuntimeMock(), resolveMatrixAuth: (...args: unknown[]) => resolveMatrixAuthMock(...args), - resolveMatrixAuthContext: (...args: unknown[]) => resolveMatrixAuthContextMock(...args), + resolveMatrixAuthContext: resolveMatrixAuthContextMock, })); vi.mock("../../runtime.js", () => ({ diff --git a/extensions/matrix/src/onboarding.test.ts b/extensions/matrix/src/onboarding.test.ts index b2d585a2e67..3de29198dc9 100644 --- a/extensions/matrix/src/onboarding.test.ts +++ b/extensions/matrix/src/onboarding.test.ts @@ -294,4 +294,37 @@ describe("matrix onboarding", () => { allowFromKey: "channels.matrix.accounts.ops.dm.allowFrom", }); }); + + it("reports configured when only the effective default Matrix account is configured", async () => { + setMatrixRuntime({ + state: { + resolveStateDir: (_env: NodeJS.ProcessEnv, homeDir?: () => string) => + (homeDir ?? (() => "/tmp"))(), + }, + config: { + loadConfig: () => ({}), + }, + } as never); + + const status = await matrixOnboardingAdapter.getStatus({ + cfg: { + channels: { + matrix: { + defaultAccount: "ops", + accounts: { + ops: { + homeserver: "https://matrix.ops.example.org", + accessToken: "ops-token", + }, + }, + }, + }, + } as CoreConfig, + accountOverrides: {}, + }); + + expect(status.configured).toBe(true); + expect(status.statusLines).toContain("Matrix: configured"); + expect(status.selectionHint).toBe("configured"); + }); }); diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 00e6e952fc0..adf9f0ad211 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -493,7 +493,10 @@ async function runMatrixConfigure(params: { export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { channel, getStatus: async ({ cfg }) => { - const account = resolveMatrixAccount({ cfg: cfg as CoreConfig }); + const account = resolveMatrixAccount({ + cfg: cfg as CoreConfig, + accountId: resolveMatrixOnboardingAccountId(cfg as CoreConfig), + }); const configured = account.configured; const sdkReady = isMatrixSdkAvailable(); return { diff --git a/src/infra/matrix-legacy-crypto.test.ts b/src/infra/matrix-legacy-crypto.test.ts index b2938aabdd9..8402c416c0c 100644 --- a/src/infra/matrix-legacy-crypto.test.ts +++ b/src/infra/matrix-legacy-crypto.test.ts @@ -295,4 +295,27 @@ describe("matrix legacy encrypted-state migration", () => { ); }); }); + + it("warns instead of throwing when a legacy crypto path is a file", async () => { + await withTempHome(async (home) => { + const stateDir = path.join(home, ".openclaw"); + writeFile(path.join(stateDir, "matrix", "crypto"), "not-a-directory"); + + const cfg: OpenClawConfig = { + channels: { + matrix: { + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "tok-123", + }, + }, + }; + + const detection = detectLegacyMatrixCrypto({ cfg, env: process.env }); + expect(detection.plans).toHaveLength(0); + expect(detection.warnings).toContain( + `Legacy Matrix encrypted state path exists but is not a directory: ${path.join(stateDir, "matrix", "crypto")}. OpenClaw skipped automatic crypto migration for that path.`, + ); + }); + }); }); diff --git a/src/infra/matrix-legacy-crypto.ts b/src/infra/matrix-legacy-crypto.ts index c63ca977fb8..28e5c692a6a 100644 --- a/src/infra/matrix-legacy-crypto.ts +++ b/src/infra/matrix-legacy-crypto.ts @@ -87,18 +87,50 @@ type MatrixStoredRecoveryKey = { }; }; -function isLegacyBotSdkCryptoStore(cryptoRootDir: string): boolean { - return ( - fs.existsSync(path.join(cryptoRootDir, "bot-sdk.json")) || - fs.existsSync(path.join(cryptoRootDir, "matrix-sdk-crypto.sqlite3")) || - fs - .readdirSync(cryptoRootDir, { withFileTypes: true }) - .some( - (entry) => - entry.isDirectory() && - fs.existsSync(path.join(cryptoRootDir, entry.name, "matrix-sdk-crypto.sqlite3")), - ) - ); +function detectLegacyBotSdkCryptoStore(cryptoRootDir: string): { + detected: boolean; + warning?: string; +} { + try { + const stat = fs.statSync(cryptoRootDir); + if (!stat.isDirectory()) { + return { + detected: false, + warning: + `Legacy Matrix encrypted state path exists but is not a directory: ${cryptoRootDir}. ` + + "OpenClaw skipped automatic crypto migration for that path.", + }; + } + } catch (err) { + return { + detected: false, + warning: + `Failed reading legacy Matrix encrypted state path (${cryptoRootDir}): ${String(err)}. ` + + "OpenClaw skipped automatic crypto migration for that path.", + }; + } + + try { + return { + detected: + fs.existsSync(path.join(cryptoRootDir, "bot-sdk.json")) || + fs.existsSync(path.join(cryptoRootDir, "matrix-sdk-crypto.sqlite3")) || + fs + .readdirSync(cryptoRootDir, { withFileTypes: true }) + .some( + (entry) => + entry.isDirectory() && + fs.existsSync(path.join(cryptoRootDir, entry.name, "matrix-sdk-crypto.sqlite3")), + ), + }; + } catch (err) { + return { + detected: false, + warning: + `Failed scanning legacy Matrix encrypted state path (${cryptoRootDir}): ${String(err)}. ` + + "OpenClaw skipped automatic crypto migration for that path.", + }; + } } function resolveMatrixAccountIds(cfg: OpenClawConfig): string[] { @@ -110,7 +142,14 @@ function resolveLegacyMatrixFlatStorePlan(params: { env: NodeJS.ProcessEnv; }): MatrixLegacyCryptoPlan | { warning: string } | null { const legacy = resolveMatrixLegacyFlatStoragePaths(resolveStateDir(params.env, os.homedir)); - if (!fs.existsSync(legacy.cryptoPath) || !isLegacyBotSdkCryptoStore(legacy.cryptoPath)) { + if (!fs.existsSync(legacy.cryptoPath)) { + return null; + } + const legacyStore = detectLegacyBotSdkCryptoStore(legacy.cryptoPath); + if (legacyStore.warning) { + return { warning: legacyStore.warning }; + } + if (!legacyStore.detected) { return null; } @@ -183,7 +222,15 @@ function resolveMatrixLegacyCryptoPlans(params: { continue; } const legacyCryptoPath = path.join(target.rootDir, "crypto"); - if (!fs.existsSync(legacyCryptoPath) || !isLegacyBotSdkCryptoStore(legacyCryptoPath)) { + if (!fs.existsSync(legacyCryptoPath)) { + continue; + } + const detectedStore = detectLegacyBotSdkCryptoStore(legacyCryptoPath); + if (detectedStore.warning) { + warnings.push(detectedStore.warning); + continue; + } + if (!detectedStore.detected) { continue; } if (