diff --git a/extensions/matrix/src/matrix/client.test.ts b/extensions/matrix/src/matrix/client.test.ts index 708fb5e3aa9..12dfc510823 100644 --- a/extensions/matrix/src/matrix/client.test.ts +++ b/extensions/matrix/src/matrix/client.test.ts @@ -28,33 +28,6 @@ vi.mock("./credentials-write.runtime.js", () => ({ touchMatrixCredentials: touchMatrixCredentialsMock, })); -let getMatrixScopedEnvVarNames: typeof import("./client/config.js").getMatrixScopedEnvVarNames; -let resolveImplicitMatrixAccountId: typeof import("./client/config.js").resolveImplicitMatrixAccountId; -let resolveMatrixConfig: typeof import("./client/config.js").resolveMatrixConfig; -let resolveMatrixConfigForAccount: typeof import("./client/config.js").resolveMatrixConfigForAccount; -let resolveMatrixAuth: typeof import("./client/config.js").resolveMatrixAuth; -let resolveMatrixAuthContext: typeof import("./client/config.js").resolveMatrixAuthContext; -let resolveValidatedMatrixHomeserverUrl: typeof import("./client/config.js").resolveValidatedMatrixHomeserverUrl; -let validateMatrixHomeserverUrl: typeof import("./client/config.js").validateMatrixHomeserverUrl; -let credentialsReadModule: typeof import("./credentials-read.js"); -let sdkModule: typeof import("./sdk.js"); - -beforeEach(async () => { - vi.resetModules(); - ({ - getMatrixScopedEnvVarNames, - resolveImplicitMatrixAccountId, - resolveMatrixConfig, - resolveMatrixConfigForAccount, - resolveMatrixAuth, - resolveMatrixAuthContext, - resolveValidatedMatrixHomeserverUrl, - validateMatrixHomeserverUrl, - } = await import("./client/config.js")); - credentialsReadModule = await import("./credentials-read.js"); - sdkModule = await import("./sdk.js"); -}); - vi.mock("matrix-js-sdk", async (importOriginal) => { const actual = await importOriginal(); return { @@ -66,6 +39,24 @@ vi.mock("matrix-js-sdk", async (importOriginal) => { }; }); +const { + getMatrixScopedEnvVarNames, + resolveImplicitMatrixAccountId, + resolveMatrixConfig, + resolveMatrixConfigForAccount, + resolveMatrixAuth, + resolveMatrixAuthContext, + resolveValidatedMatrixHomeserverUrl, + validateMatrixHomeserverUrl, +} = await import("./client/config.js"); + +let credentialsReadModule: typeof import("./credentials-read.js") | undefined; +let sdkModule: typeof import("./sdk.js") | undefined; + +beforeEach(() => { + installMatrixTestRuntime(); +}); + describe("resolveMatrixConfig", () => { it("prefers config over env", () => { const cfg = { @@ -600,18 +591,30 @@ describe("resolveMatrixConfig", () => { }); describe("resolveMatrixAuth", () => { + beforeEach(async () => { + credentialsReadModule ??= await import("./credentials-read.js"); + sdkModule ??= await import("./sdk.js"); + vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReset(); + vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReturnValue(null); + vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReset(); + vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReturnValue(false); + saveMatrixCredentialsMock.mockReset(); + touchMatrixCredentialsMock.mockReset(); + }); + afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); - saveMatrixCredentialsMock.mockReset(); }); it("uses the hardened client request path for password login and persists deviceId", async () => { - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - access_token: "tok-123", - user_id: "@bot:example.org", - device_id: "DEVICE123", - }); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + access_token: "tok-123", + user_id: "@bot:example.org", + device_id: "DEVICE123", + }); const cfg = { channels: { @@ -658,7 +661,7 @@ describe("resolveMatrixAuth", () => { }); it("surfaces password login errors when account credentials are invalid", async () => { - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest"); + const doRequestSpy = vi.spyOn(sdkModule!.MatrixClient.prototype, "doRequest"); doRequestSpy.mockRejectedValueOnce(new Error("Invalid username or password")); const cfg = { @@ -690,14 +693,14 @@ describe("resolveMatrixAuth", () => { }); it("uses cached matching credentials when access token is not configured", async () => { - vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReturnValue({ + vi.mocked(credentialsReadModule!.loadMatrixCredentials).mockReturnValue({ homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "cached-token", deviceId: "CACHEDDEVICE", createdAt: "2026-01-01T00:00:00.000Z", }); - vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReturnValue(true); + vi.mocked(credentialsReadModule!.credentialsMatchConfig).mockReturnValue(true); const cfg = { channels: { @@ -740,13 +743,13 @@ describe("resolveMatrixAuth", () => { }); it("falls back to config deviceId when cached credentials are missing it", async () => { - vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReturnValue({ + vi.mocked(credentialsReadModule!.loadMatrixCredentials).mockReturnValue({ homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "tok-123", createdAt: "2026-01-01T00:00:00.000Z", }); - vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReturnValue(true); + vi.mocked(credentialsReadModule!.credentialsMatchConfig).mockReturnValue(true); const cfg = { channels: { @@ -799,10 +802,12 @@ describe("resolveMatrixAuth", () => { }); it("resolves token-only non-default account userId from whoami instead of inheriting the base user", async () => { - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - user_id: "@ops:example.org", - device_id: "OPSDEVICE", - }); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + user_id: "@ops:example.org", + device_id: "OPSDEVICE", + }); const cfg = { channels: { @@ -831,13 +836,15 @@ describe("resolveMatrixAuth", () => { }); it("uses named-account password auth instead of inheriting the base access token", async () => { - vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReturnValue(null); - vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReturnValue(false); - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - access_token: "ops-token", - user_id: "@ops:example.org", - device_id: "OPSDEVICE", - }); + vi.mocked(credentialsReadModule!.loadMatrixCredentials).mockReturnValue(null); + vi.mocked(credentialsReadModule!.credentialsMatchConfig).mockReturnValue(false); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + access_token: "ops-token", + user_id: "@ops:example.org", + device_id: "OPSDEVICE", + }); const cfg = { channels: { @@ -881,10 +888,12 @@ describe("resolveMatrixAuth", () => { }); it("resolves missing whoami identity fields for token auth", async () => { - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - user_id: "@bot:example.org", - device_id: "DEVICE123", - }); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + user_id: "@bot:example.org", + device_id: "DEVICE123", + }); const cfg = { channels: { @@ -918,10 +927,12 @@ describe("resolveMatrixAuth", () => { await fs.writeFile(secretPath, "file-token\n", "utf8"); await fs.chmod(secretPath, 0o600); - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - user_id: "@bot:example.org", - device_id: "DEVICE123", - }); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + user_id: "@bot:example.org", + device_id: "DEVICE123", + }); try { const cfg = { @@ -961,10 +972,12 @@ describe("resolveMatrixAuth", () => { }); it("does not resolve inactive password SecretRefs when scoped token auth wins", async () => { - const doRequestSpy = vi.spyOn(sdkModule.MatrixClient.prototype, "doRequest").mockResolvedValue({ - user_id: "@ops:example.org", - device_id: "OPSDEVICE", - }); + const doRequestSpy = vi + .spyOn(sdkModule!.MatrixClient.prototype, "doRequest") + .mockResolvedValue({ + user_id: "@ops:example.org", + device_id: "OPSDEVICE", + }); const cfg = { channels: { @@ -1006,13 +1019,13 @@ describe("resolveMatrixAuth", () => { }); it("uses config deviceId with cached credentials when token is loaded from cache", async () => { - vi.mocked(credentialsReadModule.loadMatrixCredentials).mockReturnValue({ + vi.mocked(credentialsReadModule!.loadMatrixCredentials).mockReturnValue({ homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "tok-123", createdAt: "2026-01-01T00:00:00.000Z", }); - vi.mocked(credentialsReadModule.credentialsMatchConfig).mockReturnValue(true); + vi.mocked(credentialsReadModule!.credentialsMatchConfig).mockReturnValue(true); const cfg = { channels: { diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index 0b0d6016b67..30841dd9052 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -28,10 +28,25 @@ import { } from "../account-config.js"; import { resolveMatrixConfigFieldPath } from "../config-update.js"; import { credentialsMatchConfig, loadMatrixCredentials } from "../credentials-read.js"; -import { MatrixClient } from "../sdk.js"; -import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; import type { MatrixAuth, MatrixResolvedConfig } from "./types.js"; +type MatrixAuthClientDeps = { + MatrixClient: typeof import("../sdk.js").MatrixClient; + ensureMatrixSdkLoggingConfigured: typeof import("./logging.js").ensureMatrixSdkLoggingConfigured; +}; + +let matrixAuthClientDepsPromise: Promise | undefined; + +async function loadMatrixAuthClientDeps(): Promise { + matrixAuthClientDepsPromise ??= Promise.all([import("../sdk.js"), import("./logging.js")]).then( + ([sdkModule, loggingModule]) => ({ + MatrixClient: sdkModule.MatrixClient, + ensureMatrixSdkLoggingConfigured: loggingModule.ensureMatrixSdkLoggingConfigured, + }), + ); + return await matrixAuthClientDepsPromise; +} + function readEnvSecretRefFallback(params: { value: unknown; env?: NodeJS.ProcessEnv; @@ -670,6 +685,7 @@ export async function resolveMatrixAuth(params?: { if (!userId || !knownDeviceId) { // Fetch whoami when we need to resolve userId and/or deviceId from token auth. + const { MatrixClient, ensureMatrixSdkLoggingConfigured } = await loadMatrixAuthClientDeps(); ensureMatrixSdkLoggingConfigured(); const tempClient = new MatrixClient(homeserver, accessToken, { ssrfPolicy: resolved.ssrfPolicy, @@ -767,6 +783,7 @@ export async function resolveMatrixAuth(params?: { } // Login with password using the same hardened request path as other Matrix HTTP calls. + const { MatrixClient, ensureMatrixSdkLoggingConfigured } = await loadMatrixAuthClientDeps(); ensureMatrixSdkLoggingConfigured(); const loginClient = new MatrixClient(homeserver, "", { ssrfPolicy: resolved.ssrfPolicy, diff --git a/extensions/matrix/src/matrix/monitor/startup.test.ts b/extensions/matrix/src/matrix/monitor/startup.test.ts index 44d328fb811..e73d771da1c 100644 --- a/extensions/matrix/src/matrix/monitor/startup.test.ts +++ b/extensions/matrix/src/matrix/monitor/startup.test.ts @@ -1,8 +1,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { CoreConfig } from "../../types.js"; +import type { MatrixAccountPatch } from "../config-update.js"; +import type { MatrixManagedDeviceInfo } from "../device-health.js"; 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 type { MatrixStartupMaintenanceDeps } from "./startup.js"; import { runMatrixStartupMaintenance } from "./startup.js"; function createVerificationStatus( @@ -67,47 +71,32 @@ function createLegacyCryptoRestoreResult( } as MatrixLegacyCryptoRestoreResult; } -const hoisted = vi.hoisted(() => ({ - maybeRestoreLegacyMatrixBackup: vi.fn(async () => createLegacyCryptoRestoreResult()), - summarizeMatrixDeviceHealth: vi.fn(() => ({ - staleOpenClawDevices: [] as Array<{ deviceId: string }>, - })), - syncMatrixOwnProfile: vi.fn(async () => createProfileSyncResult()), - ensureMatrixStartupVerification: vi.fn(async () => createStartupVerificationOutcome("verified")), - updateMatrixAccountConfig: vi.fn((cfg: unknown) => cfg), -})); - -vi.mock("../config-update.js", () => ({ - updateMatrixAccountConfig: hoisted.updateMatrixAccountConfig, -})); - -vi.mock("../device-health.js", () => ({ - summarizeMatrixDeviceHealth: hoisted.summarizeMatrixDeviceHealth, -})); - -vi.mock("../profile.js", () => ({ - syncMatrixOwnProfile: hoisted.syncMatrixOwnProfile, -})); - -vi.mock("./legacy-crypto-restore.js", () => ({ - maybeRestoreLegacyMatrixBackup: hoisted.maybeRestoreLegacyMatrixBackup, -})); - -vi.mock("./startup-verification.js", () => ({ - ensureMatrixStartupVerification: hoisted.ensureMatrixStartupVerification, -})); +function createDeps( + overrides: Partial = {}, +): MatrixStartupMaintenanceDeps { + return { + maybeRestoreLegacyMatrixBackup: vi.fn(async () => createLegacyCryptoRestoreResult()), + summarizeMatrixDeviceHealth: vi.fn(() => ({ + currentDeviceId: null, + staleOpenClawDevices: [] as MatrixManagedDeviceInfo[], + currentOpenClawDevices: [] as MatrixManagedDeviceInfo[], + })), + syncMatrixOwnProfile: vi.fn(async () => createProfileSyncResult()), + ensureMatrixStartupVerification: vi.fn(async () => + createStartupVerificationOutcome("verified"), + ), + updateMatrixAccountConfig: vi.fn( + (cfg: CoreConfig, _accountId: string, _patch: MatrixAccountPatch) => cfg, + ), + ...overrides, + }; +} describe("runMatrixStartupMaintenance", () => { + let deps: MatrixStartupMaintenanceDeps; + beforeEach(() => { - hoisted.maybeRestoreLegacyMatrixBackup - .mockClear() - .mockResolvedValue(createLegacyCryptoRestoreResult()); - hoisted.summarizeMatrixDeviceHealth.mockClear().mockReturnValue({ staleOpenClawDevices: [] }); - hoisted.syncMatrixOwnProfile.mockClear().mockResolvedValue(createProfileSyncResult()); - hoisted.ensureMatrixStartupVerification - .mockClear() - .mockResolvedValue(createStartupVerificationOutcome("verified")); - hoisted.updateMatrixAccountConfig.mockClear().mockImplementation((cfg: unknown) => cfg); + deps = createDeps(); }); function createParams(): Parameters[0] { @@ -151,7 +140,7 @@ describe("runMatrixStartupMaintenance", () => { it("persists converted avatar URLs after profile sync", async () => { const params = createParams(); const updatedCfg = { channels: { matrix: { avatarUrl: "mxc://avatar" } } }; - hoisted.syncMatrixOwnProfile.mockResolvedValue( + vi.mocked(deps.syncMatrixOwnProfile).mockResolvedValue( createProfileSyncResult({ avatarUpdated: true, resolvedAvatarUrl: "mxc://avatar", @@ -159,18 +148,18 @@ describe("runMatrixStartupMaintenance", () => { convertedAvatarFromHttp: true, }), ); - hoisted.updateMatrixAccountConfig.mockReturnValue(updatedCfg); + vi.mocked(deps.updateMatrixAccountConfig).mockReturnValue(updatedCfg); - await runMatrixStartupMaintenance(params); + await runMatrixStartupMaintenance(params, deps); - expect(hoisted.syncMatrixOwnProfile).toHaveBeenCalledWith( + expect(deps.syncMatrixOwnProfile).toHaveBeenCalledWith( expect.objectContaining({ userId: "@bot:example.org", displayName: "Ops Bot", avatarUrl: "https://example.org/avatar.png", }), ); - expect(hoisted.updateMatrixAccountConfig).toHaveBeenCalledWith( + expect(deps.updateMatrixAccountConfig).toHaveBeenCalledWith( { channels: { matrix: {} } }, "ops", { avatarUrl: "mxc://avatar" }, @@ -184,13 +173,17 @@ describe("runMatrixStartupMaintenance", () => { it("reports stale devices, pending verification, and restored legacy backups", async () => { const params = createParams(); params.auth.encryption = true; - hoisted.summarizeMatrixDeviceHealth.mockReturnValue({ - staleOpenClawDevices: [{ deviceId: "DEV123" }], + vi.mocked(deps.summarizeMatrixDeviceHealth).mockReturnValue({ + currentDeviceId: null, + staleOpenClawDevices: [ + { deviceId: "DEV123", displayName: "OpenClaw Device", current: false }, + ], + currentOpenClawDevices: [], }); - hoisted.ensureMatrixStartupVerification.mockResolvedValue( + vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValue( createStartupVerificationOutcome("pending"), ); - hoisted.maybeRestoreLegacyMatrixBackup.mockResolvedValue( + vi.mocked(deps.maybeRestoreLegacyMatrixBackup).mockResolvedValue( createLegacyCryptoRestoreResult({ kind: "restored", imported: 2, @@ -199,7 +192,7 @@ describe("runMatrixStartupMaintenance", () => { }), ); - await runMatrixStartupMaintenance(params); + await runMatrixStartupMaintenance(params, deps); expect(params.logger.warn).toHaveBeenCalledWith( "matrix: stale OpenClaw devices detected for @bot:example.org: DEV123. Run 'openclaw matrix devices prune-stale --account ops' to keep encrypted-room trust healthy.", @@ -221,21 +214,21 @@ describe("runMatrixStartupMaintenance", () => { it("logs cooldown and request-failure verification outcomes without throwing", async () => { const params = createParams(); params.auth.encryption = true; - hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce( + vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValueOnce( createStartupVerificationOutcome("cooldown", { retryAfterMs: 321 }), ); - await runMatrixStartupMaintenance(params); + await runMatrixStartupMaintenance(params, deps); expect(params.logVerboseMessage).toHaveBeenCalledWith( "matrix: skipped startup verification request due to cooldown (retryAfterMs=321)", ); - hoisted.ensureMatrixStartupVerification.mockResolvedValueOnce( + vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValueOnce( createStartupVerificationOutcome("request-failed", { error: "boom" }), ); - await runMatrixStartupMaintenance(params); + await runMatrixStartupMaintenance(params, deps); expect(params.logger.debug).toHaveBeenCalledWith( "Matrix startup verification request failed (non-fatal)", diff --git a/extensions/matrix/src/matrix/monitor/startup.ts b/extensions/matrix/src/matrix/monitor/startup.ts index ecb5f85627a..cb888aa7f7e 100644 --- a/extensions/matrix/src/matrix/monitor/startup.ts +++ b/extensions/matrix/src/matrix/monitor/startup.ts @@ -1,12 +1,7 @@ import type { RuntimeLogger } from "../../runtime-api.js"; import type { CoreConfig, MatrixConfig } from "../../types.js"; import type { MatrixAuth } from "../client.js"; -import { updateMatrixAccountConfig } from "../config-update.js"; -import { summarizeMatrixDeviceHealth } from "../device-health.js"; -import { syncMatrixOwnProfile } from "../profile.js"; import type { MatrixClient } from "../sdk.js"; -import { maybeRestoreLegacyMatrixBackup } from "./legacy-crypto-restore.js"; -import { ensureMatrixStartupVerification } from "./startup-verification.js"; type MatrixStartupClient = Pick< MatrixClient, @@ -20,24 +15,63 @@ type MatrixStartupClient = Pick< | "uploadContent" >; -export async function runMatrixStartupMaintenance(params: { - client: MatrixStartupClient; - auth: MatrixAuth; - accountId: string; - effectiveAccountId: string; - accountConfig: MatrixConfig; - logger: RuntimeLogger; - logVerboseMessage: (message: string) => void; - loadConfig: () => CoreConfig; - writeConfigFile: (cfg: never) => Promise; - loadWebMedia: ( - url: string, - maxBytes: number, - ) => Promise<{ buffer: Buffer; contentType?: string; fileName?: string }>; - env?: NodeJS.ProcessEnv; -}): Promise { +export type MatrixStartupMaintenanceDeps = { + updateMatrixAccountConfig: typeof import("../config-update.js").updateMatrixAccountConfig; + summarizeMatrixDeviceHealth: typeof import("../device-health.js").summarizeMatrixDeviceHealth; + syncMatrixOwnProfile: typeof import("../profile.js").syncMatrixOwnProfile; + maybeRestoreLegacyMatrixBackup: typeof import("./legacy-crypto-restore.js").maybeRestoreLegacyMatrixBackup; + ensureMatrixStartupVerification: typeof import("./startup-verification.js").ensureMatrixStartupVerification; +}; + +let matrixStartupMaintenanceDepsPromise: Promise | undefined; + +async function loadMatrixStartupMaintenanceDeps(): Promise { + matrixStartupMaintenanceDepsPromise ??= Promise.all([ + import("../config-update.js"), + import("../device-health.js"), + import("../profile.js"), + import("./legacy-crypto-restore.js"), + import("./startup-verification.js"), + ]).then( + ([ + configUpdateModule, + deviceHealthModule, + profileModule, + legacyCryptoRestoreModule, + startupVerificationModule, + ]) => ({ + updateMatrixAccountConfig: configUpdateModule.updateMatrixAccountConfig, + summarizeMatrixDeviceHealth: deviceHealthModule.summarizeMatrixDeviceHealth, + syncMatrixOwnProfile: profileModule.syncMatrixOwnProfile, + maybeRestoreLegacyMatrixBackup: legacyCryptoRestoreModule.maybeRestoreLegacyMatrixBackup, + ensureMatrixStartupVerification: startupVerificationModule.ensureMatrixStartupVerification, + }), + ); + return await matrixStartupMaintenanceDepsPromise; +} + +export async function runMatrixStartupMaintenance( + params: { + client: MatrixStartupClient; + auth: MatrixAuth; + accountId: string; + effectiveAccountId: string; + accountConfig: MatrixConfig; + logger: RuntimeLogger; + logVerboseMessage: (message: string) => void; + loadConfig: () => CoreConfig; + writeConfigFile: (cfg: never) => Promise; + loadWebMedia: ( + url: string, + maxBytes: number, + ) => Promise<{ buffer: Buffer; contentType?: string; fileName?: string }>; + env?: NodeJS.ProcessEnv; + }, + deps?: MatrixStartupMaintenanceDeps, +): Promise { + const runtimeDeps = deps ?? (await loadMatrixStartupMaintenanceDeps()); try { - const profileSync = await syncMatrixOwnProfile({ + const profileSync = await runtimeDeps.syncMatrixOwnProfile({ client: params.client, userId: params.auth.userId, displayName: params.accountConfig.name, @@ -56,7 +90,7 @@ export async function runMatrixStartupMaintenance(params: { params.accountConfig.avatarUrl !== profileSync.resolvedAvatarUrl ) { const latestCfg = params.loadConfig(); - const updatedCfg = updateMatrixAccountConfig(latestCfg, params.accountId, { + const updatedCfg = runtimeDeps.updateMatrixAccountConfig(latestCfg, params.accountId, { avatarUrl: profileSync.resolvedAvatarUrl, }); await params.writeConfigFile(updatedCfg as never); @@ -73,7 +107,9 @@ export async function runMatrixStartupMaintenance(params: { } try { - const deviceHealth = summarizeMatrixDeviceHealth(await params.client.listOwnDevices()); + const deviceHealth = runtimeDeps.summarizeMatrixDeviceHealth( + await params.client.listOwnDevices(), + ); if (deviceHealth.staleOpenClawDevices.length > 0) { params.logger.warn( `matrix: stale OpenClaw devices detected for ${params.auth.userId}: ${deviceHealth.staleOpenClawDevices.map((device) => device.deviceId).join(", ")}. Run 'openclaw matrix devices prune-stale --account ${params.effectiveAccountId}' to keep encrypted-room trust healthy.`, @@ -86,7 +122,7 @@ export async function runMatrixStartupMaintenance(params: { } try { - const startupVerification = await ensureMatrixStartupVerification({ + const startupVerification = await runtimeDeps.ensureMatrixStartupVerification({ client: params.client, auth: params.auth, accountConfig: params.accountConfig, @@ -128,7 +164,7 @@ export async function runMatrixStartupMaintenance(params: { } try { - const legacyCryptoRestore = await maybeRestoreLegacyMatrixBackup({ + const legacyCryptoRestore = await runtimeDeps.maybeRestoreLegacyMatrixBackup({ client: params.client, auth: params.auth, env: params.env,