diff --git a/CHANGELOG.md b/CHANGELOG.md index 53377018e92..ac5140c81b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai - Gateway/chat: register chat.send runs in the chat run registry so lifecycle error events reach the client instead of being silently dropped, fixing stuck 'waiting' state and /abort reporting no active run. (#69747) Thanks @wangshu94. - Plugins/QQ Bot: enable the bundled qqbot plugin by default so its runtime dependency `@tencent-connect/qqbot-connector` is installed on first launch, unblocking the QR-code binding flow that dynamically imports the connector before any account is configured. (#71051) Thanks @cxyhhhhh. - Gateway/agent RPC: register active `agent` runs into the chat abort controller map so `chat.abort` and `sessions.abort` can interrupt them, matching `chat.send` behavior and unblocking external runtimes that drive the Gateway through the public `agent` RPC. Fixes #71128. (#71214) Thanks @bitloi. +- Matrix/CLI: pass resolved runtime config into verify commands, so `openclaw matrix verify status` and sibling verify subcommands no longer crash before acquiring the Matrix client. Fixes #70992. (#71102) Thanks @luyao618. ## 2026.4.23 diff --git a/extensions/matrix/src/cli.test.ts b/extensions/matrix/src/cli.test.ts index ac9b5a3db2c..b2d30804630 100644 --- a/extensions/matrix/src/cli.test.ts +++ b/extensions/matrix/src/cli.test.ts @@ -211,7 +211,9 @@ describe("matrix CLI verification commands", () => { }); const program = buildProgram(); - await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], { from: "user" }); + await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], { + from: "user", + }); expect(process.exitCode).toBe(1); }); @@ -267,6 +269,76 @@ describe("matrix CLI verification commands", () => { expect(process.exitCode).toBe(1); }); + it("passes loaded cfg to verify status action", async () => { + const fakeCfg = { channels: { matrix: {} } }; + matrixRuntimeLoadConfigMock.mockReturnValue(fakeCfg); + mockMatrixVerificationStatus({ recoveryKeyCreatedAt: null }); + const program = buildProgram(); + + await program.parseAsync(["matrix", "verify", "status"], { from: "user" }); + + expect(getMatrixVerificationStatusMock).toHaveBeenCalledWith( + expect.objectContaining({ cfg: fakeCfg }), + ); + }); + + it("passes loaded cfg to all verify subcommands", async () => { + const fakeCfg = { channels: { matrix: {} } }; + matrixRuntimeLoadConfigMock.mockReturnValue(fakeCfg); + + // verify bootstrap + const program1 = buildProgram(); + await program1.parseAsync(["matrix", "verify", "bootstrap"], { + from: "user", + }); + expect(bootstrapMatrixVerificationMock).toHaveBeenCalledWith( + expect.objectContaining({ cfg: fakeCfg }), + ); + + // verify device + verifyMatrixRecoveryKeyMock.mockResolvedValue({ success: true }); + const program2 = buildProgram(); + await program2.parseAsync(["matrix", "verify", "device", "test-key"], { + from: "user", + }); + expect(verifyMatrixRecoveryKeyMock).toHaveBeenCalledWith( + "test-key", + expect.objectContaining({ cfg: fakeCfg }), + ); + + // verify backup status + getMatrixRoomKeyBackupStatusMock.mockResolvedValue({}); + const program3 = buildProgram(); + await program3.parseAsync(["matrix", "verify", "backup", "status"], { + from: "user", + }); + expect(getMatrixRoomKeyBackupStatusMock).toHaveBeenCalledWith( + expect.objectContaining({ cfg: fakeCfg }), + ); + + // verify backup reset + const program4 = buildProgram(); + await program4.parseAsync(["matrix", "verify", "backup", "reset", "--yes"], { from: "user" }); + expect(resetMatrixRoomKeyBackupMock).toHaveBeenCalledWith( + expect.objectContaining({ cfg: fakeCfg }), + ); + + // verify backup restore + restoreMatrixRoomKeyBackupMock.mockResolvedValue({ + success: true, + imported: 0, + total: 0, + backup: {}, + }); + const program5 = buildProgram(); + await program5.parseAsync(["matrix", "verify", "backup", "restore"], { + from: "user", + }); + expect(restoreMatrixRoomKeyBackupMock).toHaveBeenCalledWith( + expect.objectContaining({ cfg: fakeCfg }), + ); + }); + it("lists matrix devices", async () => { listMatrixOwnDevicesMock.mockResolvedValue([ { @@ -332,7 +404,9 @@ describe("matrix CLI verification commands", () => { from: "user", }); - expect(pruneMatrixStaleGatewayDevicesMock).toHaveBeenCalledWith({ accountId: "poe" }); + expect(pruneMatrixStaleGatewayDevicesMock).toHaveBeenCalledWith({ + accountId: "poe", + }); expect(console.log).toHaveBeenCalledWith("Deleted stale OpenClaw devices: BritdXC6iL"); expect(console.log).toHaveBeenCalledWith("Current device: A7hWrQ70ea"); expect(console.log).toHaveBeenCalledWith("Remaining devices: 1"); @@ -452,7 +526,9 @@ describe("matrix CLI verification commands", () => { { from: "user" }, ); - expect(bootstrapMatrixVerificationMock).toHaveBeenCalledWith({ accountId: "ops" }); + expect(bootstrapMatrixVerificationMock).toHaveBeenCalledWith({ + accountId: "ops", + }); expect(console.log).toHaveBeenCalledWith("Matrix verification bootstrap: complete"); expect(console.log).toHaveBeenCalledWith( `Recovery key created at: ${formatExpectedLocalTimestamp("2026-03-09T06:00:00.000Z")}`, @@ -705,7 +781,9 @@ describe("matrix CLI verification commands", () => { }); const program = buildProgram(); - await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], { from: "user" }); + await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], { + from: "user", + }); expect(process.exitCode).toBe(0); }); @@ -715,7 +793,9 @@ describe("matrix CLI verification commands", () => { mockMatrixVerificationStatus({ recoveryKeyCreatedAt: recoveryCreatedAt }); const program = buildProgram(); - await program.parseAsync(["matrix", "verify", "status", "--verbose"], { from: "user" }); + await program.parseAsync(["matrix", "verify", "status", "--verbose"], { + from: "user", + }); expect(console.log).toHaveBeenCalledWith( `Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`, @@ -920,7 +1000,9 @@ describe("matrix CLI verification commands", () => { it("requires --yes before resetting the Matrix room-key backup", async () => { const program = buildProgram(); - await program.parseAsync(["matrix", "verify", "backup", "reset"], { from: "user" }); + await program.parseAsync(["matrix", "verify", "backup", "reset"], { + from: "user", + }); expect(process.exitCode).toBe(1); expect(resetMatrixRoomKeyBackupMock).not.toHaveBeenCalled(); @@ -936,7 +1018,10 @@ describe("matrix CLI verification commands", () => { from: "user", }); - expect(resetMatrixRoomKeyBackupMock).toHaveBeenCalledWith({ accountId: "default" }); + expect(resetMatrixRoomKeyBackupMock).toHaveBeenCalledWith({ + accountId: "default", + cfg: {}, + }); expect(console.log).toHaveBeenCalledWith("Reset success: yes"); expect(console.log).toHaveBeenCalledWith("Previous backup version: 1"); expect(console.log).toHaveBeenCalledWith("Deleted backup version: 1"); @@ -981,6 +1066,7 @@ describe("matrix CLI verification commands", () => { expect(getMatrixVerificationStatusMock).toHaveBeenCalledWith({ accountId: "assistant", + cfg: {}, includeRecoveryKey: false, }); expect(console.log).toHaveBeenCalledWith("Account: assistant"); diff --git a/extensions/matrix/src/cli.ts b/extensions/matrix/src/cli.ts index 497520951d7..a5455c14d3e 100644 --- a/extensions/matrix/src/cli.ts +++ b/extensions/matrix/src/cli.ts @@ -96,6 +96,17 @@ function resolveMatrixCliAccountId(accountId?: string): string { return resolveMatrixAuthContext({ cfg, accountId }).accountId; } +function resolveMatrixCliAccountContext(accountId?: string): { + accountId: string; + cfg: CoreConfig; +} { + const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig; + return { + accountId: resolveMatrixAuthContext({ cfg, accountId }).accountId, + cfg, + }; +} + function formatMatrixCliCommand(command: string, accountId?: string): string { const normalizedAccountId = normalizeAccountId(accountId); const suffix = normalizedAccountId === "default" ? "" : ` --account ${normalizedAccountId}`; @@ -925,13 +936,14 @@ export function registerMatrixCli(params: { program: Command }): void { includeRecoveryKey?: boolean; json?: boolean; }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixVerificationStatus({ accountId, + cfg, includeRecoveryKey: options.includeRecoveryKey === true, }), onText: (status, verbose) => { @@ -952,11 +964,11 @@ export function registerMatrixCli(params: { program: Command }): void { .option("--verbose", "Show detailed diagnostics") .option("--json", "Output as JSON") .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await getMatrixRoomKeyBackupStatus({ accountId }), + run: async () => await getMatrixRoomKeyBackupStatus({ accountId, cfg }), onText: (status, verbose) => { printAccountLabel(accountId); printBackupSummary(status); @@ -979,7 +991,7 @@ export function registerMatrixCli(params: { program: Command }): void { .option("--json", "Output as JSON") .action( async (options: { account?: string; yes?: boolean; verbose?: boolean; json?: boolean }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, @@ -987,7 +999,7 @@ export function registerMatrixCli(params: { program: Command }): void { if (options.yes !== true) { throw new Error("Refusing to reset Matrix room-key backup without --yes"); } - return await resetMatrixRoomKeyBackup({ accountId }); + return await resetMatrixRoomKeyBackup({ accountId, cfg }); }, onText: (result, verbose) => { printAccountLabel(accountId); @@ -1025,13 +1037,14 @@ export function registerMatrixCli(params: { program: Command }): void { verbose?: boolean; json?: boolean; }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await restoreMatrixRoomKeyBackup({ accountId, + cfg, recoveryKey: options.recoveryKey, }), onText: (result, verbose) => { @@ -1074,13 +1087,14 @@ export function registerMatrixCli(params: { program: Command }): void { verbose?: boolean; json?: boolean; }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await bootstrapMatrixVerification({ accountId, + cfg, recoveryKey: options.recoveryKey, forceResetCrossSigning: options.forceResetCrossSigning === true, }), @@ -1129,11 +1143,11 @@ export function registerMatrixCli(params: { program: Command }): void { .option("--json", "Output as JSON") .action( async (key: string, options: { account?: string; verbose?: boolean; json?: boolean }) => { - const accountId = resolveMatrixCliAccountId(options.account); + const { accountId, cfg } = resolveMatrixCliAccountContext(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await verifyMatrixRecoveryKey(key, { accountId }), + run: async () => await verifyMatrixRecoveryKey(key, { accountId, cfg }), onText: (result, verbose) => { printAccountLabel(accountId); if (!result.success) {