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): Promise { 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; 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>) { 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"); }); });