mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 15:01:03 +00:00
980 lines
31 KiB
TypeScript
980 lines
31 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 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();
|
|
const matrixRuntimeWriteConfigFileMock = vi.fn();
|
|
const resetMatrixRoomKeyBackupMock = vi.fn();
|
|
const restoreMatrixRoomKeyBackupMock = vi.fn();
|
|
const setMatrixSdkConsoleLoggingMock = 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),
|
|
resetMatrixRoomKeyBackup: (...args: unknown[]) => resetMatrixRoomKeyBackupMock(...args),
|
|
restoreMatrixRoomKeyBackup: (...args: unknown[]) => restoreMatrixRoomKeyBackupMock(...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),
|
|
},
|
|
}),
|
|
}));
|
|
|
|
const { registerMatrixCli } = await import("./cli.js");
|
|
|
|
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(() => {
|
|
vi.clearAllMocks();
|
|
process.exitCode = undefined;
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
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("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("lists matrix devices", async () => {
|
|
listMatrixOwnDevicesMock.mockResolvedValue([
|
|
{
|
|
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,
|
|
},
|
|
]);
|
|
const program = buildProgram();
|
|
|
|
await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" });
|
|
|
|
expect(listMatrixOwnDevicesMock).toHaveBeenCalledWith({ accountId: "poe" });
|
|
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" });
|
|
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 = (console.log as unknown as { mock: { calls: unknown[][] } }).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("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("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'.",
|
|
);
|
|
});
|
|
|
|
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" });
|
|
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",
|
|
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");
|
|
});
|
|
});
|