openclaw/extensions/matrix/src/cli.test.ts

542 lines
17 KiB
TypeScript

import { Command } from "commander";
import { formatZonedTimestamp } from "openclaw/plugin-sdk/matrix";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const bootstrapMatrixVerificationMock = vi.fn();
const getMatrixRoomKeyBackupStatusMock = vi.fn();
const getMatrixVerificationStatusMock = vi.fn();
const matrixSetupApplyAccountConfigMock = vi.fn();
const matrixSetupValidateInputMock = vi.fn();
const matrixRuntimeLoadConfigMock = vi.fn();
const matrixRuntimeWriteConfigFileMock = vi.fn();
const restoreMatrixRoomKeyBackupMock = vi.fn();
const setMatrixSdkLogModeMock = vi.fn();
const updateMatrixOwnProfileMock = vi.fn();
const verifyMatrixRecoveryKeyMock = vi.fn();
vi.mock("./matrix/actions/verification.js", () => ({
bootstrapMatrixVerification: (...args: unknown[]) => bootstrapMatrixVerificationMock(...args),
getMatrixRoomKeyBackupStatus: (...args: unknown[]) => getMatrixRoomKeyBackupStatusMock(...args),
getMatrixVerificationStatus: (...args: unknown[]) => getMatrixVerificationStatusMock(...args),
restoreMatrixRoomKeyBackup: (...args: unknown[]) => restoreMatrixRoomKeyBackupMock(...args),
verifyMatrixRecoveryKey: (...args: unknown[]) => verifyMatrixRecoveryKeyMock(...args),
}));
vi.mock("./matrix/client/logging.js", () => ({
setMatrixSdkLogMode: (...args: unknown[]) => setMatrixSdkLogModeMock(...args),
}));
vi.mock("./matrix/actions/profile.js", () => ({
updateMatrixOwnProfile: (...args: unknown[]) => updateMatrixOwnProfileMock(...args),
}));
vi.mock("./channel.js", () => ({
matrixPlugin: {
setup: {
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),
},
}),
}));
let registerMatrixCli: typeof import("./cli.js").registerMatrixCli;
function buildProgram(): Command {
const program = new Command();
registerMatrixCli({ program });
return program;
}
function formatExpectedLocalTimestamp(value: string): string {
return formatZonedTimestamp(new Date(value), { displaySeconds: true }) ?? value;
}
describe("matrix CLI verification commands", () => {
beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
process.exitCode = undefined;
({ registerMatrixCli } = await import("./cli.js"));
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
matrixSetupValidateInputMock.mockReturnValue(null);
matrixSetupApplyAccountConfigMock.mockImplementation(({ cfg }: { cfg: unknown }) => cfg);
matrixRuntimeLoadConfigMock.mockReturnValue({});
matrixRuntimeWriteConfigFileMock.mockResolvedValue(undefined);
updateMatrixOwnProfileMock.mockResolvedValue({
skipped: false,
displayNameUpdated: true,
avatarUpdated: false,
resolvedAvatarUrl: null,
convertedAvatarFromHttp: false,
});
});
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("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("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",
}),
}),
);
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("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("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(console.log).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";
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: recoveryCreatedAt,
pendingVerifications: 0,
});
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";
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: recoveryCreatedAt,
pendingVerifications: 0,
});
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("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");
});
});