mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:20:42 +00:00
Centralize the shared realtime agent consult tool for browser Talk, Google Meet, and Voice Call.
1608 lines
54 KiB
TypeScript
1608 lines
54 KiB
TypeScript
import { Command } from "commander";
|
|
import { formatZonedTimestamp } from "openclaw/plugin-sdk/matrix-runtime-shared";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { registerMatrixCli, resetMatrixCliStateForTests } from "./cli.js";
|
|
|
|
const bootstrapMatrixVerificationMock = vi.fn();
|
|
const acceptMatrixVerificationMock = vi.fn();
|
|
const cancelMatrixVerificationMock = vi.fn();
|
|
const confirmMatrixVerificationSasMock = vi.fn();
|
|
const getMatrixRoomKeyBackupStatusMock = vi.fn();
|
|
const getMatrixVerificationSasMock = vi.fn();
|
|
const getMatrixVerificationStatusMock = vi.fn();
|
|
const listMatrixOwnDevicesMock = vi.fn();
|
|
const listMatrixVerificationsMock = vi.fn();
|
|
const mismatchMatrixVerificationSasMock = vi.fn();
|
|
const pruneMatrixStaleGatewayDevicesMock = vi.fn();
|
|
const requestMatrixVerificationMock = 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();
|
|
const matrixRuntimeWriteConfigFileMock = vi.fn();
|
|
const resetMatrixRoomKeyBackupMock = vi.fn();
|
|
const restoreMatrixRoomKeyBackupMock = vi.fn();
|
|
const runMatrixSelfVerificationMock = vi.fn();
|
|
const setMatrixSdkConsoleLoggingMock = vi.fn();
|
|
const setMatrixSdkLogModeMock = vi.fn();
|
|
const startMatrixVerificationMock = vi.fn();
|
|
const updateMatrixOwnProfileMock = vi.fn();
|
|
const verifyMatrixRecoveryKeyMock = vi.fn();
|
|
const consoleLogMock = vi.fn();
|
|
const consoleErrorMock = vi.fn();
|
|
const stdoutWriteMock = vi.fn();
|
|
|
|
vi.mock("./matrix/actions/verification.js", () => ({
|
|
acceptMatrixVerification: (...args: unknown[]) => acceptMatrixVerificationMock(...args),
|
|
bootstrapMatrixVerification: (...args: unknown[]) => bootstrapMatrixVerificationMock(...args),
|
|
cancelMatrixVerification: (...args: unknown[]) => cancelMatrixVerificationMock(...args),
|
|
confirmMatrixVerificationSas: (...args: unknown[]) => confirmMatrixVerificationSasMock(...args),
|
|
getMatrixRoomKeyBackupStatus: (...args: unknown[]) => getMatrixRoomKeyBackupStatusMock(...args),
|
|
getMatrixVerificationSas: (...args: unknown[]) => getMatrixVerificationSasMock(...args),
|
|
getMatrixVerificationStatus: (...args: unknown[]) => getMatrixVerificationStatusMock(...args),
|
|
listMatrixVerifications: (...args: unknown[]) => listMatrixVerificationsMock(...args),
|
|
mismatchMatrixVerificationSas: (...args: unknown[]) => mismatchMatrixVerificationSasMock(...args),
|
|
requestMatrixVerification: (...args: unknown[]) => requestMatrixVerificationMock(...args),
|
|
resetMatrixRoomKeyBackup: (...args: unknown[]) => resetMatrixRoomKeyBackupMock(...args),
|
|
restoreMatrixRoomKeyBackup: (...args: unknown[]) => restoreMatrixRoomKeyBackupMock(...args),
|
|
runMatrixSelfVerification: (...args: unknown[]) => runMatrixSelfVerificationMock(...args),
|
|
startMatrixVerification: (...args: unknown[]) => startMatrixVerificationMock(...args),
|
|
verifyMatrixRecoveryKey: (...args: unknown[]) => verifyMatrixRecoveryKeyMock(...args),
|
|
}));
|
|
|
|
vi.mock("./matrix/actions/devices.js", () => ({
|
|
listMatrixOwnDevices: (...args: unknown[]) => listMatrixOwnDevicesMock(...args),
|
|
pruneMatrixStaleGatewayDevices: (...args: unknown[]) =>
|
|
pruneMatrixStaleGatewayDevicesMock(...args),
|
|
}));
|
|
|
|
vi.mock("./matrix/client/logging.js", () => ({
|
|
setMatrixSdkConsoleLogging: (...args: unknown[]) => setMatrixSdkConsoleLoggingMock(...args),
|
|
setMatrixSdkLogMode: (...args: unknown[]) => setMatrixSdkLogModeMock(...args),
|
|
}));
|
|
|
|
vi.mock("./matrix/actions/profile.js", () => ({
|
|
updateMatrixOwnProfile: (...args: unknown[]) => updateMatrixOwnProfileMock(...args),
|
|
}));
|
|
|
|
vi.mock("./matrix/accounts.js", () => ({
|
|
resolveMatrixAccount: (...args: unknown[]) => resolveMatrixAccountMock(...args),
|
|
resolveMatrixAccountConfig: (...args: unknown[]) => resolveMatrixAccountConfigMock(...args),
|
|
}));
|
|
|
|
vi.mock("./matrix/client.js", () => ({
|
|
resolveMatrixAuthContext: (...args: unknown[]) => resolveMatrixAuthContextMock(...args),
|
|
}));
|
|
|
|
vi.mock("./setup-core.js", () => ({
|
|
matrixSetupAdapter: {
|
|
applyAccountConfig: (...args: unknown[]) => matrixSetupApplyAccountConfigMock(...args),
|
|
validateInput: (...args: unknown[]) => matrixSetupValidateInputMock(...args),
|
|
},
|
|
}));
|
|
|
|
vi.mock("./runtime.js", () => ({
|
|
getMatrixRuntime: () => ({
|
|
config: {
|
|
loadConfig: (...args: unknown[]) => matrixRuntimeLoadConfigMock(...args),
|
|
writeConfigFile: (...args: unknown[]) => matrixRuntimeWriteConfigFileMock(...args),
|
|
},
|
|
}),
|
|
}));
|
|
|
|
function buildProgram(): Command {
|
|
const program = new Command();
|
|
registerMatrixCli({ program });
|
|
return program;
|
|
}
|
|
|
|
function formatExpectedLocalTimestamp(value: string): string {
|
|
return formatZonedTimestamp(new Date(value), { displaySeconds: true }) ?? value;
|
|
}
|
|
|
|
function mockMatrixVerificationStatus(params: {
|
|
recoveryKeyCreatedAt: string | null;
|
|
verifiedAt?: string;
|
|
}) {
|
|
getMatrixVerificationStatusMock.mockResolvedValue({
|
|
encryptionEnabled: true,
|
|
verified: true,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "1",
|
|
backup: {
|
|
serverVersion: "1",
|
|
activeVersion: "1",
|
|
trusted: true,
|
|
matchesDecryptionKey: true,
|
|
decryptionKeyCached: true,
|
|
},
|
|
recoveryKeyStored: true,
|
|
recoveryKeyCreatedAt: params.recoveryKeyCreatedAt,
|
|
pendingVerifications: 0,
|
|
verifiedAt: params.verifiedAt,
|
|
});
|
|
}
|
|
|
|
function mockMatrixVerificationSummary(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: "self-1",
|
|
transactionId: "txn-1",
|
|
otherUserId: "@bot:example.org",
|
|
otherDeviceId: "PHONE123",
|
|
isSelfVerification: true,
|
|
initiatedByMe: true,
|
|
phaseName: "started",
|
|
pending: true,
|
|
methods: ["m.sas.v1"],
|
|
chosenMethod: "m.sas.v1",
|
|
hasSas: true,
|
|
sas: {
|
|
decimal: [1234, 5678, 9012],
|
|
},
|
|
completed: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("matrix CLI verification commands", () => {
|
|
beforeEach(() => {
|
|
resetMatrixCliStateForTests();
|
|
vi.clearAllMocks();
|
|
process.exitCode = undefined;
|
|
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => consoleLogMock(...args));
|
|
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) =>
|
|
consoleErrorMock(...args),
|
|
);
|
|
vi.spyOn(process.stdout, "write").mockImplementation(((chunk: string | Uint8Array) => {
|
|
stdoutWriteMock(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
return true;
|
|
}) as typeof process.stdout.write);
|
|
consoleLogMock.mockReset();
|
|
consoleErrorMock.mockReset();
|
|
stdoutWriteMock.mockReset();
|
|
matrixSetupValidateInputMock.mockReturnValue(null);
|
|
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,
|
|
});
|
|
resolveMatrixAccountConfigMock.mockReturnValue({
|
|
encryption: false,
|
|
});
|
|
bootstrapMatrixVerificationMock.mockResolvedValue({
|
|
success: true,
|
|
verification: {
|
|
recoveryKeyCreatedAt: null,
|
|
backupVersion: null,
|
|
},
|
|
crossSigning: {},
|
|
pendingVerifications: 0,
|
|
cryptoBootstrap: {},
|
|
});
|
|
resetMatrixRoomKeyBackupMock.mockResolvedValue({
|
|
success: true,
|
|
previousVersion: "1",
|
|
deletedVersion: "1",
|
|
createdVersion: "2",
|
|
backup: {
|
|
serverVersion: "2",
|
|
activeVersion: "2",
|
|
trusted: true,
|
|
matchesDecryptionKey: true,
|
|
decryptionKeyCached: true,
|
|
keyLoadAttempted: false,
|
|
keyLoadError: null,
|
|
},
|
|
});
|
|
updateMatrixOwnProfileMock.mockResolvedValue({
|
|
skipped: false,
|
|
displayNameUpdated: true,
|
|
avatarUpdated: false,
|
|
resolvedAvatarUrl: null,
|
|
convertedAvatarFromHttp: false,
|
|
});
|
|
listMatrixOwnDevicesMock.mockResolvedValue([]);
|
|
pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({
|
|
before: [],
|
|
staleGatewayDeviceIds: [],
|
|
currentDeviceId: null,
|
|
deletedDeviceIds: [],
|
|
remainingDevices: [],
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
process.exitCode = undefined;
|
|
});
|
|
|
|
it("sets non-zero exit code for device verification failures in JSON mode", async () => {
|
|
verifyMatrixRecoveryKeyMock.mockResolvedValue({
|
|
success: false,
|
|
error: "invalid key",
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "device", "bad-key", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
|
|
it("prints recovery-key and identity-trust diagnostics for device verification failures", async () => {
|
|
verifyMatrixRecoveryKeyMock.mockResolvedValue({
|
|
success: false,
|
|
error:
|
|
"Matrix recovery key was applied, but this device still lacks full Matrix identity trust.",
|
|
encryptionEnabled: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "7",
|
|
backup: {
|
|
serverVersion: "7",
|
|
activeVersion: "7",
|
|
trusted: true,
|
|
matchesDecryptionKey: true,
|
|
decryptionKeyCached: true,
|
|
keyLoadAttempted: true,
|
|
keyLoadError: null,
|
|
},
|
|
verified: false,
|
|
localVerified: true,
|
|
crossSigningVerified: false,
|
|
signedByOwner: false,
|
|
recoveryKeyAccepted: true,
|
|
backupUsable: true,
|
|
deviceOwnerVerified: false,
|
|
recoveryKeyStored: true,
|
|
recoveryKeyCreatedAt: "2026-02-25T20:10:11.000Z",
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "device", "valid-key"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(consoleErrorMock).toHaveBeenCalledWith(
|
|
"Verification failed: Matrix recovery key was applied, but this device still lacks full Matrix identity trust.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Recovery key accepted: yes");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup usable: yes");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Device verified by owner: no");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup: active and trusted on this device");
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Recovery key can unlock the room-key backup, but full Matrix identity trust is still incomplete. Run openclaw matrix verify self and follow the prompts from another Matrix client.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- If you intend to replace the current cross-signing identity, run openclaw matrix verify bootstrap --recovery-key '<key>' --force-reset-cross-signing.",
|
|
);
|
|
});
|
|
|
|
it("runs interactive Matrix self-verification in one CLI flow", async () => {
|
|
runMatrixSelfVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
completed: true,
|
|
deviceOwnerVerified: true,
|
|
ownerVerification: {
|
|
backup: {
|
|
activeVersion: "1",
|
|
decryptionKeyCached: true,
|
|
keyLoadAttempted: false,
|
|
keyLoadError: null,
|
|
matchesDecryptionKey: true,
|
|
serverVersion: "1",
|
|
trusted: true,
|
|
},
|
|
backupVersion: "1",
|
|
crossSigningVerified: true,
|
|
deviceId: "DEVICE123",
|
|
localVerified: true,
|
|
recoveryKeyCreatedAt: null,
|
|
recoveryKeyId: null,
|
|
recoveryKeyStored: true,
|
|
signedByOwner: true,
|
|
userId: "@bot:example.org",
|
|
verified: true,
|
|
},
|
|
pending: false,
|
|
phaseName: "done",
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
["matrix", "verify", "self", "--account", "ops", "--timeout-ms", "5000"],
|
|
{
|
|
from: "user",
|
|
},
|
|
);
|
|
|
|
expect(runMatrixSelfVerificationMock).toHaveBeenCalledWith({
|
|
accountId: "ops",
|
|
cfg: {},
|
|
timeoutMs: 5000,
|
|
onRequested: expect.any(Function),
|
|
onReady: expect.any(Function),
|
|
onSas: expect.any(Function),
|
|
confirmSas: expect.any(Function),
|
|
});
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Self-verification complete.");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Device verified by owner: yes");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Cross-signing verified: yes");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Signed by owner: yes");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup: active and trusted on this device");
|
|
});
|
|
|
|
it("requests Matrix self-verification and prints the follow-up SAS commands", async () => {
|
|
requestMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "self-verify-1",
|
|
hasSas: false,
|
|
sas: undefined,
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "request", "--own-user", "--account", "ops"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(requestMatrixVerificationMock).toHaveBeenCalledWith({
|
|
accountId: "ops",
|
|
cfg: {},
|
|
ownUser: true,
|
|
userId: undefined,
|
|
deviceId: undefined,
|
|
roomId: undefined,
|
|
});
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Verification id: self-verify-1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Transaction id: txn-1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Accept the verification request in another Matrix client for this account.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Then run openclaw matrix verify start --account ops -- txn-1 to start SAS verification.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify sas --account ops -- txn-1 to display the SAS emoji or decimals.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- When the SAS matches, run openclaw matrix verify confirm-sas --account ops -- txn-1.",
|
|
);
|
|
});
|
|
|
|
it("prints DM lookup details in Matrix verification follow-up commands", async () => {
|
|
requestMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "dm-verify-1",
|
|
transactionId: "txn-dm",
|
|
roomId: "!room-'$(x):example.org",
|
|
otherUserId: "@alice:example.org",
|
|
isSelfVerification: false,
|
|
hasSas: false,
|
|
sas: undefined,
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"verify",
|
|
"request",
|
|
"--user-id",
|
|
"@alice:example.org",
|
|
"--room-id",
|
|
"!room-'$(x):example.org",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(requestMatrixVerificationMock).toHaveBeenCalledWith({
|
|
accountId: "default",
|
|
cfg: {},
|
|
ownUser: undefined,
|
|
userId: "@alice:example.org",
|
|
deviceId: undefined,
|
|
roomId: "!room-'$(x):example.org",
|
|
});
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Room id: !room-'$(x):example.org");
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Then run openclaw matrix verify start --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm to start SAS verification.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify sas --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm to display the SAS emoji or decimals.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- When the SAS matches, run openclaw matrix verify confirm-sas --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' -- txn-dm.",
|
|
);
|
|
});
|
|
|
|
it("terminates options before remote Matrix verification ids in follow-up commands", async () => {
|
|
requestMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "local-id",
|
|
transactionId: "--account=evil",
|
|
hasSas: false,
|
|
sas: undefined,
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "request", "--own-user", "--account", "ops"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Then run openclaw matrix verify start --account ops -- --account=evil to start SAS verification.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify sas --account ops -- --account=evil to display the SAS emoji or decimals.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- When the SAS matches, run openclaw matrix verify confirm-sas --account ops -- --account=evil.",
|
|
);
|
|
});
|
|
|
|
it("rejects ambiguous Matrix verification request targets", async () => {
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
["matrix", "verify", "request", "--own-user", "--user-id", "@other:example.org"],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(requestMatrixVerificationMock).not.toHaveBeenCalled();
|
|
expect(consoleErrorMock).toHaveBeenCalledWith(
|
|
"Verification request failed: --own-user cannot be combined with --user-id, --device-id, or --room-id",
|
|
);
|
|
});
|
|
|
|
it("lists Matrix verification requests", async () => {
|
|
listMatrixVerificationsMock.mockResolvedValue([
|
|
mockMatrixVerificationSummary({ id: "incoming-1", initiatedByMe: false }),
|
|
]);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "list"], { from: "user" });
|
|
|
|
expect(listMatrixVerificationsMock).toHaveBeenCalledWith({ accountId: "default", cfg: {} });
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Verification id: incoming-1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Initiated by OpenClaw: no");
|
|
});
|
|
|
|
it("sanitizes remote Matrix verification metadata before printing it", async () => {
|
|
listMatrixVerificationsMock.mockResolvedValue([
|
|
mockMatrixVerificationSummary({
|
|
id: "self-\u001B[31m1",
|
|
transactionId: "txn-\n\u009B31m1",
|
|
otherUserId: "@bot\u001B[2J\u009Dspoof\u0007:example.org",
|
|
otherDeviceId: "PHONE\r\u009B2J123",
|
|
phaseName: "started\u001B[0m",
|
|
methods: ["m.sas.v1\n\u009B31mspoof"],
|
|
chosenMethod: "m.sas.v1\u001B[1m",
|
|
sas: {
|
|
emoji: [
|
|
["🐶", "Dog\u001B[31m\u009B2J"],
|
|
["🐱", "Cat\n\u009B31mspoof"],
|
|
],
|
|
},
|
|
error: "Remote\u001B[31m cancelled\n\u009B31mforged",
|
|
}),
|
|
]);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "list"], { from: "user" });
|
|
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Verification id: self-1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Transaction id: txn-1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Other user: @bot:example.org");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Other device: PHONE123");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Phase: started");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Methods: m.sas.v1spoof");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Chosen method: m.sas.v1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("SAS emoji: 🐶 Dog | 🐱 Catspoof");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Verification error: Remote cancelledforged");
|
|
});
|
|
|
|
it("sanitizes remote Matrix status metadata before printing diagnostics", async () => {
|
|
getMatrixVerificationStatusMock.mockResolvedValue({
|
|
encryptionEnabled: true,
|
|
verified: false,
|
|
localVerified: false,
|
|
crossSigningVerified: false,
|
|
signedByOwner: false,
|
|
userId: "@bot\u001B[2J:example.org",
|
|
deviceId: "PHONE\r\u009B2J123",
|
|
backupVersion: "1\u001B[31m",
|
|
backup: {
|
|
serverVersion: "2\u001B[31m",
|
|
activeVersion: "1\u009B2J",
|
|
trusted: false,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: false,
|
|
keyLoadAttempted: true,
|
|
keyLoadError: "Remote\n\u009B31mforged",
|
|
},
|
|
recoveryKeyStored: false,
|
|
recoveryKeyCreatedAt: null,
|
|
pendingVerifications: 0,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status", "--verbose"], { from: "user" });
|
|
|
|
expect(consoleLogMock).toHaveBeenCalledWith("User: @bot:example.org");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Device: PHONE123");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup server version: 2");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup active on this device: 1");
|
|
expect(consoleLogMock).toHaveBeenCalledWith("Backup key load error: Remoteforged");
|
|
});
|
|
|
|
it("shell-quotes Matrix verification ids in follow-up command guidance", async () => {
|
|
requestMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "self-verify-1",
|
|
transactionId: "txn-'$(touch /tmp/pwn)",
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "request", "--own-user"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Then run openclaw matrix verify start -- 'txn-'\\''$(touch /tmp/pwn)' to start SAS verification.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify sas -- 'txn-'\\''$(touch /tmp/pwn)' to display the SAS emoji or decimals.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- When the SAS matches, run openclaw matrix verify confirm-sas -- 'txn-'\\''$(touch /tmp/pwn)'.",
|
|
);
|
|
});
|
|
|
|
it("shows Matrix SAS diagnostics and confirm/mismatch guidance", async () => {
|
|
getMatrixVerificationSasMock.mockResolvedValue({
|
|
decimal: [1234, 5678, 9012],
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "sas", "self-1"], { from: "user" });
|
|
|
|
expect(getMatrixVerificationSasMock).toHaveBeenCalledWith("self-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
});
|
|
expect(consoleLogMock).toHaveBeenCalledWith("SAS decimals: 1234 5678 9012");
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- If they match, run openclaw matrix verify confirm-sas -- self-1.",
|
|
);
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- If they do not match, run openclaw matrix verify mismatch-sas -- self-1.",
|
|
);
|
|
});
|
|
|
|
it("passes DM lookup details through Matrix verification follow-up commands", async () => {
|
|
startMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "dm-verify-1",
|
|
transactionId: "txn-dm",
|
|
roomId: "!dm:example.org",
|
|
otherUserId: "@alice:example.org",
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"verify",
|
|
"start",
|
|
"txn-dm",
|
|
"--user-id",
|
|
"@alice:example.org",
|
|
"--room-id",
|
|
"!dm:example.org",
|
|
"--account",
|
|
"ops",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(startMatrixVerificationMock).toHaveBeenCalledWith("txn-dm", {
|
|
accountId: "ops",
|
|
cfg: {},
|
|
method: "sas",
|
|
verificationDmUserId: "@alice:example.org",
|
|
verificationDmRoomId: "!dm:example.org",
|
|
});
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- If they match, run openclaw matrix verify confirm-sas --user-id @alice:example.org --room-id '!dm:example.org' --account ops -- txn-dm.",
|
|
);
|
|
});
|
|
|
|
it("prints stable transaction ids in follow-up commands after accepting verification", async () => {
|
|
acceptMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({
|
|
id: "verification-1",
|
|
transactionId: "txn-stable",
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "accept", "verification-1"], { from: "user" });
|
|
|
|
expect(consoleLogMock).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify start -- txn-stable to start SAS verification.",
|
|
);
|
|
});
|
|
|
|
it("confirms, rejects, accepts, starts, and cancels Matrix verification requests", async () => {
|
|
acceptMatrixVerificationMock.mockResolvedValue(mockMatrixVerificationSummary({ id: "in-1" }));
|
|
startMatrixVerificationMock.mockResolvedValue(mockMatrixVerificationSummary({ id: "in-1" }));
|
|
confirmMatrixVerificationSasMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({ id: "in-1", completed: true, pending: false }),
|
|
);
|
|
mismatchMatrixVerificationSasMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({ id: "in-1", phaseName: "cancelled", pending: false }),
|
|
);
|
|
cancelMatrixVerificationMock.mockResolvedValue(
|
|
mockMatrixVerificationSummary({ id: "in-1", phaseName: "cancelled", pending: false }),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "accept", "in-1"], { from: "user" });
|
|
await program.parseAsync(["matrix", "verify", "start", "in-1"], { from: "user" });
|
|
await program.parseAsync(["matrix", "verify", "confirm-sas", "in-1"], { from: "user" });
|
|
await program.parseAsync(["matrix", "verify", "mismatch-sas", "in-1"], { from: "user" });
|
|
await program.parseAsync(
|
|
["matrix", "verify", "cancel", "in-1", "--reason", "changed my mind"],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(acceptMatrixVerificationMock).toHaveBeenCalledWith("in-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
});
|
|
expect(startMatrixVerificationMock).toHaveBeenCalledWith("in-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
method: "sas",
|
|
});
|
|
expect(confirmMatrixVerificationSasMock).toHaveBeenCalledWith("in-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
});
|
|
expect(mismatchMatrixVerificationSasMock).toHaveBeenCalledWith("in-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
});
|
|
expect(cancelMatrixVerificationMock).toHaveBeenCalledWith("in-1", {
|
|
accountId: "default",
|
|
cfg: {},
|
|
reason: "changed my mind",
|
|
code: undefined,
|
|
});
|
|
});
|
|
|
|
it("sets non-zero exit code for bootstrap failures in JSON mode", async () => {
|
|
bootstrapMatrixVerificationMock.mockResolvedValue({
|
|
success: false,
|
|
error: "bootstrap failed",
|
|
verification: {},
|
|
crossSigning: {},
|
|
pendingVerifications: 0,
|
|
cryptoBootstrap: null,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
|
|
it("sets non-zero exit code for backup restore failures in JSON mode", async () => {
|
|
restoreMatrixRoomKeyBackupMock.mockResolvedValue({
|
|
success: false,
|
|
error: "missing backup key",
|
|
backupVersion: null,
|
|
imported: 0,
|
|
total: 0,
|
|
loadedFromSecretStorage: false,
|
|
backup: {
|
|
serverVersion: "1",
|
|
activeVersion: null,
|
|
trusted: true,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: false,
|
|
},
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "backup", "restore", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
|
|
it("sets non-zero exit code for backup reset failures in JSON mode", async () => {
|
|
resetMatrixRoomKeyBackupMock.mockResolvedValue({
|
|
success: false,
|
|
error: "reset failed",
|
|
previousVersion: "1",
|
|
deletedVersion: "1",
|
|
createdVersion: null,
|
|
backup: {
|
|
serverVersion: null,
|
|
activeVersion: null,
|
|
trusted: null,
|
|
matchesDecryptionKey: null,
|
|
decryptionKeyCached: null,
|
|
keyLoadAttempted: false,
|
|
keyLoadError: null,
|
|
},
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "backup", "reset", "--yes", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
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([
|
|
{
|
|
deviceId: "A7hWr\u001B[31mQ70ea",
|
|
displayName: "OpenClaw\u001B[2J Gateway",
|
|
lastSeenIp: "127.0.0.1\u009B2J",
|
|
lastSeenTs: 1_741_507_200_000,
|
|
current: true,
|
|
},
|
|
{
|
|
deviceId: "BritdXC6iL",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: null,
|
|
lastSeenTs: null,
|
|
current: false,
|
|
},
|
|
]);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" });
|
|
|
|
expect(listMatrixOwnDevicesMock).toHaveBeenCalledWith({ accountId: "poe", cfg: {} });
|
|
expect(console.log).toHaveBeenCalledWith("Account: poe");
|
|
expect(console.log).toHaveBeenCalledWith("- A7hWrQ70ea (current, OpenClaw Gateway)");
|
|
expect(console.log).toHaveBeenCalledWith(" Last IP: 127.0.0.1");
|
|
expect(console.log).toHaveBeenCalledWith("- BritdXC6iL (OpenClaw Gateway)");
|
|
});
|
|
|
|
it("prunes stale matrix gateway devices", async () => {
|
|
pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({
|
|
before: [
|
|
{
|
|
deviceId: "A7hWrQ70ea",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: "127.0.0.1",
|
|
lastSeenTs: 1_741_507_200_000,
|
|
current: true,
|
|
},
|
|
{
|
|
deviceId: "BritdXC6iL",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: null,
|
|
lastSeenTs: null,
|
|
current: false,
|
|
},
|
|
],
|
|
staleGatewayDeviceIds: ["BritdXC6iL"],
|
|
currentDeviceId: "A7hWrQ70ea",
|
|
deletedDeviceIds: ["BritdXC6iL"],
|
|
remainingDevices: [
|
|
{
|
|
deviceId: "A7hWrQ70ea",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: "127.0.0.1",
|
|
lastSeenTs: 1_741_507_200_000,
|
|
current: true,
|
|
},
|
|
],
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "devices", "prune-stale", "--account", "poe"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(pruneMatrixStaleGatewayDevicesMock).toHaveBeenCalledWith({
|
|
accountId: "poe",
|
|
cfg: {},
|
|
});
|
|
expect(console.log).toHaveBeenCalledWith("Deleted stale OpenClaw devices: BritdXC6iL");
|
|
expect(console.log).toHaveBeenCalledWith("Current device: A7hWrQ70ea");
|
|
expect(console.log).toHaveBeenCalledWith("Remaining devices: 1");
|
|
});
|
|
|
|
it("adds a matrix account and prints a binding hint", async () => {
|
|
matrixRuntimeLoadConfigMock.mockReturnValue({ channels: {} });
|
|
matrixSetupApplyAccountConfigMock.mockImplementation(
|
|
({ cfg, accountId }: { cfg: Record<string, unknown>; accountId: string }) => ({
|
|
...cfg,
|
|
channels: {
|
|
...(cfg.channels as Record<string, unknown> | undefined),
|
|
matrix: {
|
|
accounts: {
|
|
[accountId]: {
|
|
homeserver: "https://matrix.example.org",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--account",
|
|
"Ops",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@ops:example.org",
|
|
"--password",
|
|
"secret",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(matrixSetupValidateInputMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "ops",
|
|
input: expect.objectContaining({
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@ops:example.org",
|
|
password: "secret", // pragma: allowlist secret
|
|
}),
|
|
}),
|
|
);
|
|
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: expect.objectContaining({
|
|
homeserver: "https://matrix.example.org",
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith("Saved matrix account: ops");
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"Bind this account to an agent: openclaw agents bind --agent <id> --bind matrix:ops",
|
|
);
|
|
});
|
|
|
|
it("bootstraps verification for newly added encrypted accounts", async () => {
|
|
resolveMatrixAccountConfigMock.mockReturnValue({
|
|
encryption: true,
|
|
});
|
|
listMatrixOwnDevicesMock.mockResolvedValue([
|
|
{
|
|
deviceId: "BritdXC6iL",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: null,
|
|
lastSeenTs: null,
|
|
current: false,
|
|
},
|
|
{
|
|
deviceId: "du314Zpw3A",
|
|
displayName: "OpenClaw Gateway",
|
|
lastSeenIp: null,
|
|
lastSeenTs: null,
|
|
current: true,
|
|
},
|
|
]);
|
|
bootstrapMatrixVerificationMock.mockResolvedValue({
|
|
success: true,
|
|
verification: {
|
|
recoveryKeyCreatedAt: "2026-03-09T06:00:00.000Z",
|
|
backupVersion: "7",
|
|
},
|
|
crossSigning: {},
|
|
pendingVerifications: 0,
|
|
cryptoBootstrap: {},
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--account",
|
|
"ops",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@ops:example.org",
|
|
"--password",
|
|
"secret",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
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")}`,
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith("Backup version: 7");
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"Matrix device hygiene warning: stale OpenClaw devices detected (BritdXC6iL). Run openclaw matrix devices prune-stale --account ops.",
|
|
);
|
|
});
|
|
|
|
it("does not bootstrap verification when updating an already configured account", async () => {
|
|
matrixRuntimeLoadConfigMock.mockReturnValue({
|
|
channels: {
|
|
matrix: {
|
|
accounts: {
|
|
ops: {
|
|
enabled: true,
|
|
homeserver: "https://matrix.example.org",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
resolveMatrixAccountConfigMock.mockReturnValue({
|
|
encryption: true,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--account",
|
|
"ops",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@ops:example.org",
|
|
"--password",
|
|
"secret",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(bootstrapMatrixVerificationMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("warns instead of failing when device-health probing fails after saving the account", async () => {
|
|
listMatrixOwnDevicesMock.mockRejectedValue(new Error("homeserver unavailable"));
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--account",
|
|
"ops",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@ops:example.org",
|
|
"--password",
|
|
"secret",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
|
|
expect(process.exitCode).toBeUndefined();
|
|
expect(console.log).toHaveBeenCalledWith("Saved matrix account: ops");
|
|
expect(console.error).toHaveBeenCalledWith(
|
|
"Matrix device health warning: homeserver unavailable",
|
|
);
|
|
});
|
|
|
|
it("returns device-health warnings in JSON mode without failing the account add command", async () => {
|
|
listMatrixOwnDevicesMock.mockRejectedValue(new Error("homeserver unavailable"));
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--account",
|
|
"ops",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@ops:example.org",
|
|
"--password",
|
|
"secret",
|
|
"--json",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
|
|
expect(process.exitCode).toBeUndefined();
|
|
const jsonOutput = stdoutWriteMock.mock.calls.at(-1)?.[0];
|
|
expect(typeof jsonOutput).toBe("string");
|
|
expect(JSON.parse(String(jsonOutput))).toEqual(
|
|
expect.objectContaining({
|
|
accountId: "ops",
|
|
deviceHealth: expect.objectContaining({
|
|
currentDeviceId: null,
|
|
staleOpenClawDeviceIds: [],
|
|
error: "homeserver unavailable",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses --name as fallback account id and prints account-scoped config path", async () => {
|
|
matrixRuntimeLoadConfigMock.mockReturnValue({ channels: {} });
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--name",
|
|
"Main Bot",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--user-id",
|
|
"@main:example.org",
|
|
"--password",
|
|
"secret",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(matrixSetupValidateInputMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "main-bot",
|
|
}),
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith("Saved matrix account: main-bot");
|
|
expect(console.log).toHaveBeenCalledWith("Config path: channels.matrix.accounts.main-bot");
|
|
expect(updateMatrixOwnProfileMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "main-bot",
|
|
displayName: "Main Bot",
|
|
}),
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"Bind this account to an agent: openclaw agents bind --agent <id> --bind matrix:main-bot",
|
|
);
|
|
});
|
|
|
|
it("forwards --avatar-url through account add setup and profile sync", async () => {
|
|
matrixRuntimeLoadConfigMock.mockReturnValue({ channels: {} });
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"account",
|
|
"add",
|
|
"--name",
|
|
"Ops Bot",
|
|
"--homeserver",
|
|
"https://matrix.example.org",
|
|
"--access-token",
|
|
"ops-token",
|
|
"--avatar-url",
|
|
"mxc://example/ops-avatar",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(matrixSetupApplyAccountConfigMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "ops-bot",
|
|
input: expect.objectContaining({
|
|
name: "Ops Bot",
|
|
homeserver: "https://matrix.example.org",
|
|
accessToken: "ops-token",
|
|
avatarUrl: "mxc://example/ops-avatar",
|
|
}),
|
|
}),
|
|
);
|
|
expect(updateMatrixOwnProfileMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "ops-bot",
|
|
displayName: "Ops Bot",
|
|
avatarUrl: "mxc://example/ops-avatar",
|
|
}),
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith("Saved matrix account: ops-bot");
|
|
expect(console.log).toHaveBeenCalledWith("Config path: channels.matrix.accounts.ops-bot");
|
|
});
|
|
|
|
it("sets profile name and avatar via profile set command", async () => {
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(
|
|
[
|
|
"matrix",
|
|
"profile",
|
|
"set",
|
|
"--account",
|
|
"alerts",
|
|
"--name",
|
|
"Alerts Bot",
|
|
"--avatar-url",
|
|
"mxc://example/avatar",
|
|
],
|
|
{ from: "user" },
|
|
);
|
|
|
|
expect(updateMatrixOwnProfileMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
accountId: "alerts",
|
|
displayName: "Alerts Bot",
|
|
avatarUrl: "mxc://example/avatar",
|
|
}),
|
|
);
|
|
expect(matrixRuntimeWriteConfigFileMock).toHaveBeenCalled();
|
|
expect(console.log).toHaveBeenCalledWith("Account: alerts");
|
|
expect(console.log).toHaveBeenCalledWith("Config path: channels.matrix.accounts.alerts");
|
|
});
|
|
|
|
it("returns JSON errors for invalid account setup input", async () => {
|
|
matrixSetupValidateInputMock.mockReturnValue("Matrix requires --homeserver");
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "account", "add", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(stdoutWriteMock).toHaveBeenCalledWith(
|
|
expect.stringContaining('"error": "Matrix requires --homeserver"'),
|
|
);
|
|
});
|
|
|
|
it("keeps zero exit code for successful bootstrap in JSON mode", async () => {
|
|
process.exitCode = 0;
|
|
bootstrapMatrixVerificationMock.mockResolvedValue({
|
|
success: true,
|
|
verification: {},
|
|
crossSigning: {},
|
|
pendingVerifications: 0,
|
|
cryptoBootstrap: {},
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "bootstrap", "--json"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(0);
|
|
});
|
|
|
|
it("prints local timezone timestamps for verify status output in verbose mode", async () => {
|
|
const recoveryCreatedAt = "2026-02-25T20:10:11.000Z";
|
|
mockMatrixVerificationStatus({ recoveryKeyCreatedAt: recoveryCreatedAt });
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status", "--verbose"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
`Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`,
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith("Diagnostics:");
|
|
expect(console.log).toHaveBeenCalledWith("Locally trusted: yes");
|
|
expect(console.log).toHaveBeenCalledWith("Signed by owner: yes");
|
|
expect(setMatrixSdkLogModeMock).toHaveBeenCalledWith("default");
|
|
});
|
|
|
|
it("prints local timezone timestamps for verify bootstrap and device output in verbose mode", async () => {
|
|
const recoveryCreatedAt = "2026-02-25T20:10:11.000Z";
|
|
const verifiedAt = "2026-02-25T20:14:00.000Z";
|
|
bootstrapMatrixVerificationMock.mockResolvedValue({
|
|
success: true,
|
|
verification: {
|
|
encryptionEnabled: true,
|
|
verified: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "1",
|
|
backup: {
|
|
serverVersion: "1",
|
|
activeVersion: "1",
|
|
trusted: true,
|
|
matchesDecryptionKey: true,
|
|
decryptionKeyCached: true,
|
|
},
|
|
recoveryKeyStored: true,
|
|
recoveryKeyId: "SSSS",
|
|
recoveryKeyCreatedAt: recoveryCreatedAt,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
},
|
|
crossSigning: {
|
|
published: true,
|
|
masterKeyPublished: true,
|
|
selfSigningKeyPublished: true,
|
|
userSigningKeyPublished: true,
|
|
},
|
|
pendingVerifications: 0,
|
|
cryptoBootstrap: {},
|
|
});
|
|
verifyMatrixRecoveryKeyMock.mockResolvedValue({
|
|
success: true,
|
|
encryptionEnabled: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "1",
|
|
backup: {
|
|
serverVersion: "1",
|
|
activeVersion: "1",
|
|
trusted: true,
|
|
matchesDecryptionKey: true,
|
|
decryptionKeyCached: true,
|
|
},
|
|
verified: true,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
recoveryKeyStored: true,
|
|
recoveryKeyId: "SSSS",
|
|
recoveryKeyCreatedAt: recoveryCreatedAt,
|
|
verifiedAt,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "bootstrap", "--verbose"], {
|
|
from: "user",
|
|
});
|
|
await program.parseAsync(["matrix", "verify", "device", "valid-key", "--verbose"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
`Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`,
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
`Verified at: ${formatExpectedLocalTimestamp(verifiedAt)}`,
|
|
);
|
|
});
|
|
|
|
it("keeps default output concise when verbose is not provided", async () => {
|
|
const recoveryCreatedAt = "2026-02-25T20:10:11.000Z";
|
|
mockMatrixVerificationStatus({ recoveryKeyCreatedAt: recoveryCreatedAt });
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status"], { from: "user" });
|
|
|
|
expect(console.log).not.toHaveBeenCalledWith(
|
|
`Recovery key created at: ${formatExpectedLocalTimestamp(recoveryCreatedAt)}`,
|
|
);
|
|
expect(console.log).not.toHaveBeenCalledWith("Pending verifications: 0");
|
|
expect(console.log).not.toHaveBeenCalledWith("Diagnostics:");
|
|
expect(console.log).toHaveBeenCalledWith("Backup: active and trusted on this device");
|
|
expect(setMatrixSdkLogModeMock).toHaveBeenCalledWith("quiet");
|
|
});
|
|
|
|
it("shows explicit backup issue in default status output", async () => {
|
|
getMatrixVerificationStatusMock.mockResolvedValue({
|
|
encryptionEnabled: true,
|
|
verified: true,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "5256",
|
|
backup: {
|
|
serverVersion: "5256",
|
|
activeVersion: null,
|
|
trusted: true,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: false,
|
|
keyLoadAttempted: true,
|
|
keyLoadError: null,
|
|
},
|
|
recoveryKeyStored: true,
|
|
recoveryKeyCreatedAt: "2026-02-25T20:10:11.000Z",
|
|
pendingVerifications: 0,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status"], { from: "user" });
|
|
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"Backup issue: backup decryption key is not loaded on this device (secret storage did not return a key)",
|
|
);
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"- Backup key is not loaded on this device. Run openclaw matrix verify backup restore to load it and restore old room keys.",
|
|
);
|
|
expect(console.log).not.toHaveBeenCalledWith(
|
|
"- Backup is present but not trusted for this device. Re-run 'openclaw matrix verify device <key>'.",
|
|
);
|
|
});
|
|
|
|
it("includes key load failure details in status output", async () => {
|
|
getMatrixVerificationStatusMock.mockResolvedValue({
|
|
encryptionEnabled: true,
|
|
verified: true,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "5256",
|
|
backup: {
|
|
serverVersion: "5256",
|
|
activeVersion: null,
|
|
trusted: true,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: false,
|
|
keyLoadAttempted: true,
|
|
keyLoadError: "secret storage key is not available",
|
|
},
|
|
recoveryKeyStored: true,
|
|
recoveryKeyCreatedAt: "2026-02-25T20:10:11.000Z",
|
|
pendingVerifications: 0,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status"], { from: "user" });
|
|
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"Backup issue: backup decryption key could not be loaded from secret storage (secret storage key is not available)",
|
|
);
|
|
});
|
|
|
|
it("includes backup reset guidance when the backup key does not match this device", async () => {
|
|
getMatrixVerificationStatusMock.mockResolvedValue({
|
|
encryptionEnabled: true,
|
|
verified: true,
|
|
localVerified: true,
|
|
crossSigningVerified: true,
|
|
signedByOwner: true,
|
|
userId: "@bot:example.org",
|
|
deviceId: "DEVICE123",
|
|
backupVersion: "21868",
|
|
backup: {
|
|
serverVersion: "21868",
|
|
activeVersion: "21868",
|
|
trusted: true,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: true,
|
|
keyLoadAttempted: false,
|
|
keyLoadError: null,
|
|
},
|
|
recoveryKeyStored: true,
|
|
recoveryKeyCreatedAt: "2026-03-09T14:40:00.000Z",
|
|
pendingVerifications: 0,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "status"], { from: "user" });
|
|
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"- If you want a fresh backup baseline and accept losing unrecoverable history, run openclaw matrix verify backup reset --yes. This may also repair secret storage so the new backup key can be loaded after restart.",
|
|
);
|
|
});
|
|
|
|
it("requires --yes before resetting the Matrix room-key backup", async () => {
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "backup", "reset"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(resetMatrixRoomKeyBackupMock).not.toHaveBeenCalled();
|
|
expect(console.error).toHaveBeenCalledWith(
|
|
"Backup reset failed: Refusing to reset Matrix room-key backup without --yes",
|
|
);
|
|
});
|
|
|
|
it("resets the Matrix room-key backup when confirmed", async () => {
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "backup", "reset", "--yes"], {
|
|
from: "user",
|
|
});
|
|
|
|
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");
|
|
expect(console.log).toHaveBeenCalledWith("Current backup version: 2");
|
|
expect(console.log).toHaveBeenCalledWith("Backup: active and trusted on this device");
|
|
});
|
|
|
|
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",
|
|
cfg: {},
|
|
includeRecoveryKey: false,
|
|
});
|
|
expect(console.log).toHaveBeenCalledWith("Account: assistant");
|
|
expect(console.log).toHaveBeenCalledWith(
|
|
"- Run openclaw matrix verify device '<key>' --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",
|
|
activeVersion: null,
|
|
trusted: true,
|
|
matchesDecryptionKey: false,
|
|
decryptionKeyCached: false,
|
|
keyLoadAttempted: true,
|
|
keyLoadError: null,
|
|
});
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "verify", "backup", "status", "--verbose"], {
|
|
from: "user",
|
|
});
|
|
|
|
expect(console.log).toHaveBeenCalledWith("Backup server version: 2");
|
|
expect(console.log).toHaveBeenCalledWith("Backup active on this device: no");
|
|
expect(console.log).toHaveBeenCalledWith("Backup trusted by this device: yes");
|
|
});
|
|
});
|