File size: 4,902 Bytes
fc93158 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 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");
});
});
|