Files
openclaw/src/config/io.compat.test.ts
Sid 3002f13ca7 feat(config): add openclaw config validate and improve startup error messages (#31220)
Merged via squash.

Prepared head SHA: 4598f2a541f0bde300a096ef51638408d273c4bd
Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-03-02 00:45:51 -05:00

170 lines
5.5 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { createConfigIO } from "./io.js";
async function withTempHome(run: (home: string) => Promise<void>): Promise<void> {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
try {
await run(home);
} finally {
await fs.rm(home, { recursive: true, force: true });
}
}
async function writeConfig(
home: string,
dirname: ".openclaw",
port: number,
filename: string = "openclaw.json",
) {
const dir = path.join(home, dirname);
await fs.mkdir(dir, { recursive: true });
const configPath = path.join(dir, filename);
await fs.writeFile(configPath, JSON.stringify({ gateway: { port } }, null, 2));
return configPath;
}
function createIoForHome(home: string, env: NodeJS.ProcessEnv = {} as NodeJS.ProcessEnv) {
return createConfigIO({
env,
homedir: () => home,
});
}
describe("config io paths", () => {
it("uses ~/.openclaw/openclaw.json when config exists", async () => {
await withTempHome(async (home) => {
const configPath = await writeConfig(home, ".openclaw", 19001);
const io = createIoForHome(home);
expect(io.configPath).toBe(configPath);
expect(io.loadConfig().gateway?.port).toBe(19001);
});
});
it("defaults to ~/.openclaw/openclaw.json when config is missing", async () => {
await withTempHome(async (home) => {
const io = createIoForHome(home);
expect(io.configPath).toBe(path.join(home, ".openclaw", "openclaw.json"));
});
});
it("uses OPENCLAW_HOME for default config path", async () => {
await withTempHome(async (home) => {
const io = createConfigIO({
env: { OPENCLAW_HOME: path.join(home, "svc-home") } as NodeJS.ProcessEnv,
homedir: () => path.join(home, "ignored-home"),
});
expect(io.configPath).toBe(path.join(home, "svc-home", ".openclaw", "openclaw.json"));
});
});
it("honors explicit OPENCLAW_CONFIG_PATH override", async () => {
await withTempHome(async (home) => {
const customPath = await writeConfig(home, ".openclaw", 20002, "custom.json");
const io = createIoForHome(home, { OPENCLAW_CONFIG_PATH: customPath } as NodeJS.ProcessEnv);
expect(io.configPath).toBe(customPath);
expect(io.loadConfig().gateway?.port).toBe(20002);
});
});
it("honors legacy CLAWDBOT_CONFIG_PATH override", async () => {
await withTempHome(async (home) => {
const customPath = await writeConfig(home, ".openclaw", 20003, "legacy-custom.json");
const io = createIoForHome(home, { CLAWDBOT_CONFIG_PATH: customPath } as NodeJS.ProcessEnv);
expect(io.configPath).toBe(customPath);
expect(io.loadConfig().gateway?.port).toBe(20003);
});
});
it("normalizes safe-bin config entries at config load time", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify(
{
tools: {
exec: {
safeBinTrustedDirs: [" /custom/bin ", "", "/custom/bin", "/agent/bin"],
safeBinProfiles: {
" MyFilter ": {
allowedValueFlags: ["--limit", " --limit ", ""],
},
},
},
},
agents: {
list: [
{
id: "ops",
tools: {
exec: {
safeBinTrustedDirs: [" /ops/bin ", "/ops/bin"],
safeBinProfiles: {
" Custom ": {
deniedFlags: ["-f", " -f ", ""],
},
},
},
},
},
],
},
},
null,
2,
),
"utf-8",
);
const io = createIoForHome(home);
expect(io.configPath).toBe(configPath);
const cfg = io.loadConfig();
expect(cfg.tools?.exec?.safeBinProfiles).toEqual({
myfilter: {
allowedValueFlags: ["--limit"],
},
});
expect(cfg.tools?.exec?.safeBinTrustedDirs).toEqual(["/custom/bin", "/agent/bin"]);
expect(cfg.agents?.list?.[0]?.tools?.exec?.safeBinProfiles).toEqual({
custom: {
deniedFlags: ["-f"],
},
});
expect(cfg.agents?.list?.[0]?.tools?.exec?.safeBinTrustedDirs).toEqual(["/ops/bin"]);
});
});
it("logs invalid config path details and returns empty config", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify({ gateway: { port: "not-a-number" } }, null, 2),
);
const logger = {
warn: vi.fn(),
error: vi.fn(),
};
const io = createConfigIO({
env: {} as NodeJS.ProcessEnv,
homedir: () => home,
logger,
});
expect(io.loadConfig()).toEqual({});
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(`Invalid config at ${configPath}:\\n`),
);
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("- gateway.port:"));
});
});
});