Files
Moltbot/src/cli/program/config-guard.test.ts
2026-03-02 10:09:49 -06:00

152 lines
4.8 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { RuntimeEnv } from "../../runtime.js";
const loadAndMaybeMigrateDoctorConfigMock = vi.hoisted(() => vi.fn());
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("../../commands/doctor-config-flow.js", () => ({
loadAndMaybeMigrateDoctorConfig: loadAndMaybeMigrateDoctorConfigMock,
}));
vi.mock("../../config/config.js", () => ({
readConfigFileSnapshot: readConfigFileSnapshotMock,
}));
function makeSnapshot() {
return {
exists: false,
valid: true,
issues: [],
legacyIssues: [],
path: "/tmp/openclaw.json",
};
}
function makeRuntime() {
return {
error: vi.fn(),
exit: vi.fn(),
};
}
async function withCapturedStdout(run: () => Promise<void>): Promise<string> {
const writes: string[] = [];
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
writes.push(String(chunk));
return true;
}) as typeof process.stdout.write);
try {
await run();
return writes.join("");
} finally {
writeSpy.mockRestore();
}
}
describe("ensureConfigReady", () => {
let ensureConfigReady: (params: {
runtime: RuntimeEnv;
commandPath?: string[];
suppressDoctorStdout?: boolean;
}) => Promise<void>;
let resetConfigGuardStateForTests: () => void;
async function runEnsureConfigReady(commandPath: string[], suppressDoctorStdout = false) {
const runtime = makeRuntime();
await ensureConfigReady({ runtime: runtime as never, commandPath, suppressDoctorStdout });
return runtime;
}
function setInvalidSnapshot(overrides?: Partial<ReturnType<typeof makeSnapshot>>) {
readConfigFileSnapshotMock.mockResolvedValue({
...makeSnapshot(),
exists: true,
valid: false,
issues: [{ path: "channels.whatsapp", message: "invalid" }],
...overrides,
});
}
beforeAll(async () => {
({
ensureConfigReady,
__test__: { resetConfigGuardStateForTests },
} = await import("./config-guard.js"));
});
beforeEach(() => {
vi.clearAllMocks();
resetConfigGuardStateForTests();
readConfigFileSnapshotMock.mockResolvedValue(makeSnapshot());
});
it.each([
{
name: "skips doctor flow for read-only fast path commands",
commandPath: ["status"],
expectedDoctorCalls: 0,
},
{
name: "runs doctor flow for commands that may mutate state",
commandPath: ["message"],
expectedDoctorCalls: 1,
},
])("$name", async ({ commandPath, expectedDoctorCalls }) => {
await runEnsureConfigReady(commandPath);
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(expectedDoctorCalls);
});
it("exits for invalid config on non-allowlisted commands", async () => {
setInvalidSnapshot();
const runtime = await runEnsureConfigReady(["message"]);
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Config invalid"));
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("doctor --fix"));
expect(runtime.exit).toHaveBeenCalledWith(1);
});
it("does not exit for invalid config on allowlisted commands", async () => {
setInvalidSnapshot();
const statusRuntime = await runEnsureConfigReady(["status"]);
expect(statusRuntime.exit).not.toHaveBeenCalled();
const gatewayRuntime = await runEnsureConfigReady(["gateway", "health"]);
expect(gatewayRuntime.exit).not.toHaveBeenCalled();
});
it("runs doctor migration flow only once per module instance", async () => {
const runtimeA = makeRuntime();
const runtimeB = makeRuntime();
await ensureConfigReady({ runtime: runtimeA as never, commandPath: ["message"] });
await ensureConfigReady({ runtime: runtimeB as never, commandPath: ["message"] });
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(1);
});
it("still runs doctor flow when stdout suppression is enabled", async () => {
await runEnsureConfigReady(["message"], true);
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledTimes(1);
});
it("prevents preflight stdout noise when suppression is enabled", async () => {
loadAndMaybeMigrateDoctorConfigMock.mockImplementation(async () => {
process.stdout.write("Doctor warnings\n");
});
const output = await withCapturedStdout(async () => {
await runEnsureConfigReady(["message"], true);
});
expect(output).not.toContain("Doctor warnings");
});
it("allows preflight stdout noise when suppression is not enabled", async () => {
loadAndMaybeMigrateDoctorConfigMock.mockImplementation(async () => {
process.stdout.write("Doctor warnings\n");
});
const output = await withCapturedStdout(async () => {
await runEnsureConfigReady(["message"], false);
});
expect(output).toContain("Doctor warnings");
});
});