mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 10:22:32 +00:00
202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { ConfigFileSnapshot } from "../config/types.openclaw.js";
|
|
|
|
const loadConfigMock = vi.fn<() => OpenClawConfig>();
|
|
const readConfigFileSnapshotMock = vi.fn<() => Promise<ConfigFileSnapshot>>();
|
|
const cleanStaleMatrixPluginConfigMock = vi.fn();
|
|
|
|
vi.mock("../config/config.js", () => ({
|
|
loadConfig: () => loadConfigMock(),
|
|
readConfigFileSnapshot: () => readConfigFileSnapshotMock(),
|
|
}));
|
|
|
|
vi.mock("../commands/doctor/providers/matrix.js", () => ({
|
|
cleanStaleMatrixPluginConfig: (cfg: OpenClawConfig) => cleanStaleMatrixPluginConfigMock(cfg),
|
|
}));
|
|
|
|
const { loadConfigForInstall } = await import("./plugins-install-command.js");
|
|
|
|
function makeSnapshot(overrides: Partial<ConfigFileSnapshot> = {}): ConfigFileSnapshot {
|
|
return {
|
|
path: "/tmp/config.json5",
|
|
exists: true,
|
|
raw: '{ "plugins": {} }',
|
|
parsed: { plugins: {} },
|
|
resolved: { plugins: {} } as OpenClawConfig,
|
|
valid: false,
|
|
config: { plugins: {} } as OpenClawConfig,
|
|
hash: "abc",
|
|
issues: [{ path: "plugins.installs.matrix", message: "stale path" }],
|
|
warnings: [],
|
|
legacyIssues: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("loadConfigForInstall", () => {
|
|
const matrixNpmRequest = {
|
|
rawSpec: "@openclaw/matrix",
|
|
normalizedSpec: "@openclaw/matrix",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
loadConfigMock.mockReset();
|
|
readConfigFileSnapshotMock.mockReset();
|
|
cleanStaleMatrixPluginConfigMock.mockReset();
|
|
|
|
cleanStaleMatrixPluginConfigMock.mockImplementation((cfg: OpenClawConfig) => ({
|
|
config: cfg,
|
|
changes: [],
|
|
}));
|
|
});
|
|
|
|
it("returns the config directly when loadConfig succeeds", async () => {
|
|
const cfg = { plugins: { entries: { matrix: { enabled: true } } } } as OpenClawConfig;
|
|
loadConfigMock.mockReturnValue(cfg);
|
|
|
|
const result = await loadConfigForInstall(matrixNpmRequest);
|
|
expect(result).toBe(cfg);
|
|
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not run stale Matrix cleanup on the happy path", async () => {
|
|
const cfg = { plugins: {} } as OpenClawConfig;
|
|
loadConfigMock.mockReturnValue(cfg);
|
|
|
|
const result = await loadConfigForInstall(matrixNpmRequest);
|
|
expect(cleanStaleMatrixPluginConfigMock).not.toHaveBeenCalled();
|
|
expect(result).toBe(cfg);
|
|
});
|
|
|
|
it("falls back to snapshot config for explicit Matrix reinstall when issues match the known upgrade failure", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
const snapshotCfg = {
|
|
plugins: { installs: { matrix: { source: "path", installPath: "/gone" } } },
|
|
} as unknown as OpenClawConfig;
|
|
readConfigFileSnapshotMock.mockResolvedValue(
|
|
makeSnapshot({
|
|
parsed: { plugins: { installs: { matrix: {} } } },
|
|
config: snapshotCfg,
|
|
issues: [
|
|
{ path: "channels.matrix", message: "unknown channel id: matrix" },
|
|
{ path: "plugins.load.paths", message: "plugin: plugin path not found: /gone" },
|
|
],
|
|
}),
|
|
);
|
|
|
|
const result = await loadConfigForInstall(matrixNpmRequest);
|
|
expect(readConfigFileSnapshotMock).toHaveBeenCalled();
|
|
expect(cleanStaleMatrixPluginConfigMock).toHaveBeenCalledWith(snapshotCfg);
|
|
expect(result).toBe(snapshotCfg);
|
|
});
|
|
|
|
it("allows explicit repo-checkout Matrix reinstall recovery", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
const snapshotCfg = { plugins: {} } as OpenClawConfig;
|
|
readConfigFileSnapshotMock.mockResolvedValue(
|
|
makeSnapshot({
|
|
config: snapshotCfg,
|
|
issues: [{ path: "channels.matrix", message: "unknown channel id: matrix" }],
|
|
}),
|
|
);
|
|
|
|
const result = await loadConfigForInstall({
|
|
rawSpec: "./extensions/matrix",
|
|
normalizedSpec: "./extensions/matrix",
|
|
resolvedPath: "/tmp/repo/extensions/matrix",
|
|
});
|
|
expect(result).toBe(snapshotCfg);
|
|
});
|
|
|
|
it("rejects unrelated invalid config even during Matrix reinstall", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
readConfigFileSnapshotMock.mockResolvedValue(
|
|
makeSnapshot({
|
|
issues: [{ path: "models.default", message: "invalid model ref" }],
|
|
}),
|
|
);
|
|
|
|
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
|
|
"Config invalid outside the Matrix upgrade recovery path",
|
|
);
|
|
});
|
|
|
|
it("rejects non-Matrix install requests when config is invalid", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
await expect(
|
|
loadConfigForInstall({
|
|
rawSpec: "alpha",
|
|
normalizedSpec: "alpha",
|
|
}),
|
|
).rejects.toThrow("Config invalid; run `openclaw doctor --fix` before installing plugins.");
|
|
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws when loadConfig fails with INVALID_CONFIG and snapshot parsed is empty", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
readConfigFileSnapshotMock.mockResolvedValue(
|
|
makeSnapshot({
|
|
parsed: {},
|
|
config: {} as OpenClawConfig,
|
|
}),
|
|
);
|
|
|
|
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
|
|
"Config file could not be parsed; run `openclaw doctor` to repair it.",
|
|
);
|
|
});
|
|
|
|
it("throws when loadConfig fails with INVALID_CONFIG and config file does not exist", async () => {
|
|
const invalidConfigErr = new Error("config invalid");
|
|
(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw invalidConfigErr;
|
|
});
|
|
|
|
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot({ exists: false, parsed: {} }));
|
|
|
|
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
|
|
"Config file could not be parsed; run `openclaw doctor` to repair it.",
|
|
);
|
|
});
|
|
|
|
it("re-throws non-config errors from loadConfig", async () => {
|
|
const fsErr = new Error("EACCES: permission denied");
|
|
(fsErr as { code?: string }).code = "EACCES";
|
|
loadConfigMock.mockImplementation(() => {
|
|
throw fsErr;
|
|
});
|
|
|
|
await expect(loadConfigForInstall(matrixNpmRequest)).rejects.toThrow(
|
|
"EACCES: permission denied",
|
|
);
|
|
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|