diff --git a/extensions/matrix/src/cli.test.ts b/extensions/matrix/src/cli.test.ts index 43468374c3a..a1bb7c45b55 100644 --- a/extensions/matrix/src/cli.test.ts +++ b/extensions/matrix/src/cli.test.ts @@ -9,6 +9,7 @@ const listMatrixOwnDevicesMock = vi.fn(); const pruneMatrixStaleGatewayDevicesMock = vi.fn(); const resolveMatrixAccountConfigMock = vi.fn(); const resolveMatrixAccountMock = vi.fn(); +const resolveMatrixAuthContextMock = vi.fn(); const matrixSetupApplyAccountConfigMock = vi.fn(); const matrixSetupValidateInputMock = vi.fn(); const matrixRuntimeLoadConfigMock = vi.fn(); @@ -47,6 +48,10 @@ vi.mock("./matrix/accounts.js", () => ({ resolveMatrixAccountConfig: (...args: unknown[]) => resolveMatrixAccountConfigMock(...args), })); +vi.mock("./matrix/client.js", () => ({ + resolveMatrixAuthContext: (...args: unknown[]) => resolveMatrixAuthContextMock(...args), +})); + vi.mock("./channel.js", () => ({ matrixPlugin: { setup: { @@ -89,6 +94,14 @@ describe("matrix CLI verification commands", () => { matrixSetupApplyAccountConfigMock.mockImplementation(({ cfg }: { cfg: unknown }) => cfg); matrixRuntimeLoadConfigMock.mockReturnValue({}); matrixRuntimeWriteConfigFileMock.mockResolvedValue(undefined); + resolveMatrixAuthContextMock.mockImplementation( + ({ cfg, accountId }: { cfg: unknown; accountId?: string | null }) => ({ + cfg, + env: process.env, + accountId: accountId ?? "default", + resolved: {}, + }), + ); resolveMatrixAccountMock.mockReturnValue({ configured: false, }); @@ -719,6 +732,54 @@ describe("matrix CLI verification commands", () => { ); }); + it("prints resolved account-aware guidance when a named Matrix account is selected implicitly", async () => { + resolveMatrixAuthContextMock.mockImplementation( + ({ cfg, accountId }: { cfg: unknown; accountId?: string | null }) => ({ + cfg, + env: process.env, + accountId: accountId ?? "assistant", + resolved: {}, + }), + ); + getMatrixVerificationStatusMock.mockResolvedValue({ + encryptionEnabled: true, + verified: false, + localVerified: false, + crossSigningVerified: false, + signedByOwner: false, + userId: "@bot:example.org", + deviceId: "DEVICE123", + backupVersion: null, + backup: { + serverVersion: null, + activeVersion: null, + trusted: null, + matchesDecryptionKey: null, + decryptionKeyCached: null, + keyLoadAttempted: false, + keyLoadError: null, + }, + recoveryKeyStored: false, + recoveryKeyCreatedAt: null, + pendingVerifications: 0, + }); + const program = buildProgram(); + + await program.parseAsync(["matrix", "verify", "status"], { from: "user" }); + + expect(getMatrixVerificationStatusMock).toHaveBeenCalledWith({ + accountId: "assistant", + includeRecoveryKey: false, + }); + expect(console.log).toHaveBeenCalledWith("Account: assistant"); + expect(console.log).toHaveBeenCalledWith( + "- Run 'openclaw matrix verify device --account assistant' to verify this device.", + ); + expect(console.log).toHaveBeenCalledWith( + "- Run 'openclaw matrix verify bootstrap --account assistant' to create a room key backup.", + ); + }); + it("prints backup health lines for verify backup status in verbose mode", async () => { getMatrixRoomKeyBackupStatusMock.mockResolvedValue({ serverVersion: "2", diff --git a/extensions/matrix/src/cli.ts b/extensions/matrix/src/cli.ts index e8c56178f8e..191b5929382 100644 --- a/extensions/matrix/src/cli.ts +++ b/extensions/matrix/src/cli.ts @@ -15,6 +15,7 @@ import { restoreMatrixRoomKeyBackup, verifyMatrixRecoveryKey, } from "./matrix/actions/verification.js"; +import { resolveMatrixAuthContext } from "./matrix/client.js"; import { setMatrixSdkConsoleLogging, setMatrixSdkLogMode } from "./matrix/client/logging.js"; import { resolveMatrixConfigPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; import { isOpenClawManagedMatrixDevice } from "./matrix/device-health.js"; @@ -69,6 +70,17 @@ function printAccountLabel(accountId?: string): void { console.log(`Account: ${normalizeAccountId(accountId)}`); } +function resolveMatrixCliAccountId(accountId?: string): string { + const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig; + return resolveMatrixAuthContext({ cfg, accountId }).accountId; +} + +function formatMatrixCliCommand(command: string, accountId?: string): string { + const normalizedAccountId = normalizeAccountId(accountId); + const suffix = normalizedAccountId === "default" ? "" : ` --account ${normalizedAccountId}`; + return `openclaw matrix ${command}${suffix}`; +} + function printMatrixOwnDevices( devices: Array<{ deviceId: string; @@ -445,8 +457,8 @@ function printVerificationTrustDiagnostics(status: { console.log(`Signed by owner: ${status.signedByOwner ? "yes" : "no"}`); } -function printVerificationGuidance(status: MatrixCliVerificationStatus): void { - printGuidance(buildVerificationGuidance(status)); +function printVerificationGuidance(status: MatrixCliVerificationStatus, accountId?: string): void { + printGuidance(buildVerificationGuidance(status, accountId)); } function resolveBackupIssue(backup: MatrixCliBackupStatus): MatrixCliBackupIssue { @@ -526,15 +538,22 @@ function printBackupSummary(backup: MatrixCliBackupStatus): void { } } -function buildVerificationGuidance(status: MatrixCliVerificationStatus): string[] { +function buildVerificationGuidance( + status: MatrixCliVerificationStatus, + accountId?: string, +): string[] { const backup = resolveBackupStatus(status); const backupIssue = resolveBackupIssue(backup); const nextSteps = new Set(); if (!status.verified) { - nextSteps.add("Run 'openclaw matrix verify device ' to verify this device."); + nextSteps.add( + `Run '${formatMatrixCliCommand("verify device ", accountId)}' to verify this device.`, + ); } if (backupIssue.code === "missing-server-backup") { - nextSteps.add("Run 'openclaw matrix verify bootstrap' to create a room key backup."); + nextSteps.add( + `Run '${formatMatrixCliCommand("verify bootstrap", accountId)}' to create a room key backup.`, + ); } else if ( backupIssue.code === "key-load-failed" || backupIssue.code === "key-not-loaded" || @@ -542,24 +561,24 @@ function buildVerificationGuidance(status: MatrixCliVerificationStatus): string[ ) { if (status.recoveryKeyStored) { nextSteps.add( - "Backup key is not loaded on this device. Run 'openclaw matrix verify backup restore' to load it and restore old room keys.", + `Backup key is not loaded on this device. Run '${formatMatrixCliCommand("verify backup restore", accountId)}' to load it and restore old room keys.`, ); } else { nextSteps.add( - "Store a recovery key with 'openclaw matrix verify device ', then run 'openclaw matrix verify backup restore'.", + `Store a recovery key with '${formatMatrixCliCommand("verify device ", accountId)}', then run '${formatMatrixCliCommand("verify backup restore", accountId)}'.`, ); } } else if (backupIssue.code === "key-mismatch") { nextSteps.add( - "Backup key mismatch on this device. Re-run 'openclaw matrix verify device ' with the matching recovery key.", + `Backup key mismatch on this device. Re-run '${formatMatrixCliCommand("verify device ", accountId)}' with the matching recovery key.`, ); } else if (backupIssue.code === "untrusted-signature") { nextSteps.add( - "Backup trust chain is not verified on this device. Re-run 'openclaw matrix verify device '.", + `Backup trust chain is not verified on this device. Re-run '${formatMatrixCliCommand("verify device ", accountId)}'.`, ); } else if (backupIssue.code === "indeterminate") { nextSteps.add( - "Run 'openclaw matrix verify status --verbose' to inspect backup trust diagnostics.", + `Run '${formatMatrixCliCommand("verify status --verbose", accountId)}' to inspect backup trust diagnostics.`, ); } if (status.pendingVerifications > 0) { @@ -578,7 +597,11 @@ function printGuidance(lines: string[]): void { } } -function printVerificationStatus(status: MatrixCliVerificationStatus, verbose = false): void { +function printVerificationStatus( + status: MatrixCliVerificationStatus, + verbose = false, + accountId?: string, +): void { console.log(`Verified by owner: ${status.verified ? "yes" : "no"}`); const backup = resolveBackupStatus(status); const backupIssue = resolveBackupIssue(backup); @@ -597,7 +620,7 @@ function printVerificationStatus(status: MatrixCliVerificationStatus, verbose = } else { console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`); } - printVerificationGuidance(status); + printVerificationGuidance(status, accountId); } export function registerMatrixCli(params: { program: Command }): void { @@ -762,17 +785,18 @@ export function registerMatrixCli(params: { program: Command }): void { includeRecoveryKey?: boolean; json?: boolean; }) => { + const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await getMatrixVerificationStatus({ - accountId: options.account, + accountId, includeRecoveryKey: options.includeRecoveryKey === true, }), onText: (status, verbose) => { - printAccountLabel(options.account); - printVerificationStatus(status, verbose); + printAccountLabel(accountId); + printVerificationStatus(status, verbose, accountId); }, errorPrefix: "Error", }); @@ -788,12 +812,13 @@ 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); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await getMatrixRoomKeyBackupStatus({ accountId: options.account }), + run: async () => await getMatrixRoomKeyBackupStatus({ accountId }), onText: (status, verbose) => { - printAccountLabel(options.account); + printAccountLabel(accountId); printBackupSummary(status); if (verbose) { printBackupStatus(status); @@ -817,16 +842,17 @@ export function registerMatrixCli(params: { program: Command }): void { verbose?: boolean; json?: boolean; }) => { + const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await restoreMatrixRoomKeyBackup({ - accountId: options.account, + accountId, recoveryKey: options.recoveryKey, }), onText: (result, verbose) => { - printAccountLabel(options.account); + printAccountLabel(accountId); console.log(`Restore success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${result.error}`); @@ -865,17 +891,18 @@ export function registerMatrixCli(params: { program: Command }): void { verbose?: boolean; json?: boolean; }) => { + const accountId = resolveMatrixCliAccountId(options.account); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, run: async () => await bootstrapMatrixVerification({ - accountId: options.account, + accountId, recoveryKey: options.recoveryKey, forceResetCrossSigning: options.forceResetCrossSigning === true, }), onText: (result, verbose) => { - printAccountLabel(options.account); + printAccountLabel(accountId); console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`); if (result.error) { console.log(`Error: ${result.error}`); @@ -896,10 +923,13 @@ export function registerMatrixCli(params: { program: Command }): void { ); printVerificationBackupSummary(result.verification); } - printVerificationGuidance({ - ...result.verification, - pendingVerifications: result.pendingVerifications, - }); + printVerificationGuidance( + { + ...result.verification, + pendingVerifications: result.pendingVerifications, + }, + accountId, + ); }, shouldFail: (result) => !result.success, errorPrefix: "Verification bootstrap failed", @@ -916,12 +946,13 @@ 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); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await verifyMatrixRecoveryKey(key, { accountId: options.account }), + run: async () => await verifyMatrixRecoveryKey(key, { accountId }), onText: (result, verbose) => { - printAccountLabel(options.account); + printAccountLabel(accountId); if (!result.success) { console.error(`Verification failed: ${result.error ?? "unknown error"}`); return; @@ -935,10 +966,13 @@ export function registerMatrixCli(params: { program: Command }): void { printTimestamp("Recovery key created at", result.recoveryKeyCreatedAt); printTimestamp("Verified at", result.verifiedAt); } - printVerificationGuidance({ - ...result, - pendingVerifications: 0, - }); + printVerificationGuidance( + { + ...result, + pendingVerifications: 0, + }, + accountId, + ); }, shouldFail: (result) => !result.success, errorPrefix: "Verification failed", @@ -956,12 +990,13 @@ 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); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await listMatrixOwnDevices({ accountId: options.account }), + run: async () => await listMatrixOwnDevices({ accountId }), onText: (result) => { - printAccountLabel(options.account); + printAccountLabel(accountId); printMatrixOwnDevices(result); }, errorPrefix: "Device listing failed", @@ -975,12 +1010,13 @@ 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); await runMatrixCliCommand({ verbose: options.verbose === true, json: options.json === true, - run: async () => await pruneMatrixStaleGatewayDevices({ accountId: options.account }), + run: async () => await pruneMatrixStaleGatewayDevices({ accountId }), onText: (result, verbose) => { - printAccountLabel(options.account); + printAccountLabel(accountId); console.log( `Deleted stale OpenClaw devices: ${result.deletedDeviceIds.length ? result.deletedDeviceIds.join(", ") : "none"}`, );