mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 00:21:59 +00:00
236 lines
7.4 KiB
TypeScript
236 lines
7.4 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
applyMatrixDoctorRepair,
|
|
cleanStaleMatrixPluginConfig,
|
|
collectMatrixInstallPathWarnings,
|
|
formatMatrixLegacyCryptoPreview,
|
|
formatMatrixLegacyStatePreview,
|
|
matrixDoctor,
|
|
runMatrixDoctorSequence,
|
|
} from "./doctor.js";
|
|
|
|
vi.mock("./matrix-migration.runtime.js", async () => {
|
|
const actual = await vi.importActual<typeof import("./matrix-migration.runtime.js")>(
|
|
"./matrix-migration.runtime.js",
|
|
);
|
|
return {
|
|
...actual,
|
|
hasActionableMatrixMigration: vi.fn(() => false),
|
|
hasPendingMatrixMigration: vi.fn(() => false),
|
|
maybeCreateMatrixMigrationSnapshot: vi.fn(),
|
|
autoMigrateLegacyMatrixState: vi.fn(async () => ({ changes: [], warnings: [] })),
|
|
autoPrepareLegacyMatrixCrypto: vi.fn(async () => ({ changes: [], warnings: [] })),
|
|
};
|
|
});
|
|
|
|
describe("matrix doctor", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("formats state and crypto previews", () => {
|
|
expect(
|
|
formatMatrixLegacyStatePreview({
|
|
accountId: "default",
|
|
legacyStoragePath: "/tmp/legacy-sync.json",
|
|
targetStoragePath: "/tmp/new-sync.json",
|
|
legacyCryptoPath: "/tmp/legacy-crypto.json",
|
|
targetCryptoPath: "/tmp/new-crypto.json",
|
|
selectionNote: "Picked the newest account.",
|
|
targetRootDir: "/tmp/account-root",
|
|
}),
|
|
).toContain("Matrix plugin upgraded in place.");
|
|
|
|
const previews = formatMatrixLegacyCryptoPreview({
|
|
warnings: ["matrix warning"],
|
|
plans: [
|
|
{
|
|
accountId: "default",
|
|
rootDir: "/tmp/account-root",
|
|
homeserver: "https://matrix.example.org",
|
|
userId: "@bot:example.org",
|
|
accessToken: "tok-123",
|
|
deviceId: "DEVICE123",
|
|
legacyCryptoPath: "/tmp/legacy-crypto.json",
|
|
recoveryKeyPath: "/tmp/recovery-key.txt",
|
|
statePath: "/tmp/state.json",
|
|
},
|
|
],
|
|
});
|
|
expect(previews[0]).toBe("- matrix warning");
|
|
expect(previews[1]).toContain("/tmp/recovery-key.txt");
|
|
});
|
|
|
|
it("warns on stale custom Matrix plugin paths and cleans them", async () => {
|
|
const missingPath = path.join(tmpdir(), `openclaw-matrix-missing-${Date.now()}`);
|
|
await fs.rm(missingPath, { recursive: true, force: true });
|
|
|
|
const warnings = await collectMatrixInstallPathWarnings({
|
|
plugins: {
|
|
installs: {
|
|
matrix: { source: "path", sourcePath: missingPath, installPath: missingPath },
|
|
},
|
|
},
|
|
});
|
|
expect(warnings[0]).toContain("custom path that no longer exists");
|
|
|
|
const cleaned = await cleanStaleMatrixPluginConfig({
|
|
plugins: {
|
|
installs: {
|
|
matrix: { source: "path", sourcePath: missingPath, installPath: missingPath },
|
|
},
|
|
load: { paths: [missingPath, "/other/path"] },
|
|
allow: ["matrix", "other-plugin"],
|
|
},
|
|
});
|
|
expect(cleaned.changes[0]).toContain("Removed stale Matrix plugin references");
|
|
expect(cleaned.config.plugins?.load?.paths).toEqual(["/other/path"]);
|
|
expect(cleaned.config.plugins?.allow).toEqual(["other-plugin"]);
|
|
});
|
|
|
|
it("surfaces matrix sequence warnings and repair changes", async () => {
|
|
const runtimeApi = await import("./matrix-migration.runtime.js");
|
|
vi.mocked(runtimeApi.hasActionableMatrixMigration).mockReturnValue(true);
|
|
vi.mocked(runtimeApi.maybeCreateMatrixMigrationSnapshot).mockResolvedValue({
|
|
archivePath: "/tmp/matrix-backup.tgz",
|
|
created: true,
|
|
markerPath: "/tmp/marker.json",
|
|
});
|
|
vi.mocked(runtimeApi.autoMigrateLegacyMatrixState).mockResolvedValue({
|
|
migrated: true,
|
|
changes: ["Migrated legacy sync state"],
|
|
warnings: [],
|
|
});
|
|
vi.mocked(runtimeApi.autoPrepareLegacyMatrixCrypto).mockResolvedValue({
|
|
migrated: true,
|
|
changes: ["Prepared recovery key export"],
|
|
warnings: [],
|
|
});
|
|
|
|
const cfg = {
|
|
channels: {
|
|
matrix: {},
|
|
},
|
|
} as never;
|
|
|
|
const repair = await applyMatrixDoctorRepair({ cfg, env: process.env });
|
|
expect(repair.changes.join("\n")).toContain("Matrix migration snapshot");
|
|
|
|
const sequence = await runMatrixDoctorSequence({
|
|
cfg,
|
|
env: process.env,
|
|
shouldRepair: true,
|
|
});
|
|
expect(sequence.changeNotes.join("\n")).toContain("Matrix migration snapshot");
|
|
});
|
|
|
|
it("normalizes legacy Matrix room allow aliases to enabled", () => {
|
|
const normalize = matrixDoctor.normalizeCompatibilityConfig;
|
|
expect(normalize).toBeDefined();
|
|
if (!normalize) {
|
|
return;
|
|
}
|
|
|
|
const result = normalize({
|
|
cfg: {
|
|
channels: {
|
|
matrix: {
|
|
groups: {
|
|
"!ops:example.org": {
|
|
allow: true,
|
|
},
|
|
},
|
|
accounts: {
|
|
work: {
|
|
rooms: {
|
|
"!legacy:example.org": {
|
|
allow: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
const matrixConfig = result.config.channels?.matrix as
|
|
| {
|
|
groups?: Record<string, unknown>;
|
|
accounts?: Record<string, unknown>;
|
|
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
|
}
|
|
| undefined;
|
|
const workAccount = matrixConfig?.accounts?.work as
|
|
| {
|
|
rooms?: Record<string, unknown>;
|
|
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
|
}
|
|
| undefined;
|
|
|
|
expect(matrixConfig?.groups?.["!ops:example.org"]).toEqual({
|
|
enabled: true,
|
|
});
|
|
expect(workAccount?.rooms?.["!legacy:example.org"]).toEqual({
|
|
enabled: false,
|
|
});
|
|
expect(result.changes).toEqual(
|
|
expect.arrayContaining([
|
|
"Moved channels.matrix.groups.!ops:example.org.allow → channels.matrix.groups.!ops:example.org.enabled (true).",
|
|
"Moved channels.matrix.accounts.work.rooms.!legacy:example.org.allow → channels.matrix.accounts.work.rooms.!legacy:example.org.enabled (false).",
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("normalizes legacy Matrix private-network aliases", () => {
|
|
const normalize = matrixDoctor.normalizeCompatibilityConfig;
|
|
expect(normalize).toBeDefined();
|
|
if (!normalize) {
|
|
return;
|
|
}
|
|
|
|
const result = normalize({
|
|
cfg: {
|
|
channels: {
|
|
matrix: {
|
|
allowPrivateNetwork: true,
|
|
accounts: {
|
|
work: {
|
|
allowPrivateNetwork: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as never,
|
|
});
|
|
|
|
const matrixConfig = result.config.channels?.matrix as
|
|
| {
|
|
accounts?: Record<string, unknown>;
|
|
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
|
}
|
|
| undefined;
|
|
const workAccount = matrixConfig?.accounts?.work as
|
|
| {
|
|
network?: { dangerouslyAllowPrivateNetwork?: boolean };
|
|
}
|
|
| undefined;
|
|
|
|
expect(matrixConfig?.network).toEqual({
|
|
dangerouslyAllowPrivateNetwork: true,
|
|
});
|
|
expect(workAccount?.network).toEqual({
|
|
dangerouslyAllowPrivateNetwork: false,
|
|
});
|
|
expect(result.changes).toEqual(
|
|
expect.arrayContaining([
|
|
"Moved channels.matrix.allowPrivateNetwork → channels.matrix.network.dangerouslyAllowPrivateNetwork (true).",
|
|
"Moved channels.matrix.accounts.work.allowPrivateNetwork → channels.matrix.accounts.work.network.dangerouslyAllowPrivateNetwork (false).",
|
|
]),
|
|
);
|
|
});
|
|
});
|