fix(matrix): harden recovery QA diagnostics

This commit is contained in:
Gustavo Madeira Santana
2026-04-24 20:57:05 -04:00
parent 45d6a2fccf
commit cba60755b6
4 changed files with 42 additions and 6 deletions

View File

@@ -543,6 +543,7 @@ Docs: https://docs.openclaw.ai
- Slack: route native stream fallback replies through the normal chunked sender so long buffered Slack Connect responses are not dropped or duplicated. (#71124) Thanks @martingarramon.
- WhatsApp: transcribe accepted voice notes before agent dispatch while keeping spoken transcripts out of command authorization. (#64120) Thanks @rogerdigital.
- Plugins/CLI: expose channel plugin CLI descriptors during discovery-mode plugin loads so snapshot registries keep channel commands visible without activating full runtimes. (#71309) Thanks @gumadeiras.
- Matrix: separate recovery-key, backup, and owner-trust diagnostics during E2EE recovery, add recovery-key rotation for backup reset, and cover destructive backup restore paths in QA. (#71311) Thanks @gumadeiras.
- WhatsApp: deliver media generated by tool-result replies while still suppressing text-only tool chatter. (#60968) Thanks @adaclaw.
- Config/agents: accept `agents.list[].contextTokens` in strict config validation so per-agent overrides survive hot reload, letting `/status` reflect the configured model window instead of the 200k fallback. Fixes #70692. (#71247) Thanks @statxc.
- Heartbeat: include async exec completion details in heartbeat prompts so command-finished notifications relay the actual output. (#71213) Thanks @GodsBoy.

View File

@@ -1579,6 +1579,36 @@ describe("MatrixClient crypto bootstrapping", () => {
expect(status.serverDeviceKnown).toBe(false);
});
it("keeps verification diagnostics when the homeserver device list cannot be read", async () => {
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
matrixJsClient.getDevices = vi.fn(async () => {
throw new Error("device list unavailable");
});
matrixJsClient.getCrypto = vi.fn(() => ({
on: vi.fn(),
bootstrapCrossSigning: vi.fn(async () => {}),
bootstrapSecretStorage: vi.fn(async () => {}),
requestOwnUserVerification: vi.fn(async () => null),
getDeviceVerificationStatus: vi.fn(async () => ({
isVerified: () => true,
localVerified: true,
crossSigningVerified: true,
signedByOwner: true,
})),
}));
const client = new MatrixClient("https://matrix.example.org", "token", {
encryption: true,
});
await client.start();
const status = await client.getOwnDeviceVerificationStatus();
expect(status.verified).toBe(true);
expect(status.backup).toBeDefined();
expect(status.serverDeviceKnown).toBeNull();
});
it("does not treat local-only trust as Matrix identity trust", async () => {
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");

View File

@@ -1130,10 +1130,10 @@ export class MatrixClient {
const [backup, deviceVerification, ownDevices] = await Promise.all([
this.getRoomKeyBackupStatus(),
this.getDeviceVerificationStatus(userId, deviceId),
this.listOwnDevices(),
this.listOwnDevices().catch(() => null),
]);
const serverDeviceKnown = deviceId
? ownDevices.some((device) => device.deviceId === deviceId)
? (ownDevices?.some((device) => device.deviceId === deviceId) ?? null)
: null;
return {

View File

@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { chmod, copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import type { MatrixVerificationSummary } from "@openclaw/matrix/test-api.js";
@@ -15,6 +15,7 @@ import {
} from "./scenario-catalog.js";
import {
createMatrixQaOpenClawCliRuntime,
formatMatrixQaCliCommand,
redactMatrixQaCliOutput,
type MatrixQaCliRunResult,
} from "./scenario-runtime-cli.js";
@@ -273,13 +274,13 @@ function parseMatrixQaCliJson(result: MatrixQaCliRunResult): unknown {
const stderr = result.stderr.trim();
const payload = stdout || stderr;
if (!payload) {
throw new Error(`openclaw ${result.args.join(" ")} did not print JSON`);
throw new Error(`${formatMatrixQaCliCommand(result.args)} did not print JSON`);
}
try {
return JSON.parse(payload) as unknown;
} catch (error) {
throw new Error(
`openclaw ${result.args.join(" ")} printed invalid JSON: ${
`${formatMatrixQaCliCommand(result.args)} printed invalid JSON: ${
error instanceof Error ? error.message : String(error)
}\n${redactMatrixQaCliOutput(payload)}`,
{ cause: error },
@@ -603,10 +604,14 @@ async function mutateMatrixQaCliStateLoss(params: {
}) {
const accountRoot = await findMatrixQaCliAccountRoot(params);
const recoveryKeyPath = path.join(accountRoot, "recovery-key.json");
const preservedRecoveryKeyPath = path.join(params.runtime.rootDir, "preserved-recovery-key.json");
const preservedRecoveryKeyPath = path.join(
params.runtime.stateDir,
"preserved-recovery-key.json",
);
let recoveryKeyPreserved = false;
if (params.preserveRecoveryKey) {
await copyFile(recoveryKeyPath, preservedRecoveryKeyPath);
await chmod(preservedRecoveryKeyPath, 0o600).catch(() => undefined);
recoveryKeyPreserved = true;
}
await rm(accountRoot, { force: true, recursive: true });