Files
openclaw/src/config/io.compat.test.ts
2026-03-11 16:33:18 -07:00

216 lines
7.2 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 throws on invalid 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()).toThrow(/Invalid config/);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining(`Invalid config at ${configPath}:\\n`),
);
expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("- gateway.port:"));
});
});
it("auto-migrates legacy tlon install specs during load and snapshot reads", 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(
{
plugins: {
installs: {
tlon: {
source: "npm",
spec: "@openclaw/tlon@2026.2.21",
resolvedName: "@openclaw/tlon",
resolvedVersion: "2026.2.21",
resolvedSpec: "@openclaw/tlon@2026.2.21",
integrity: "sha512-old",
shasum: "old",
resolvedAt: "2026-03-01T00:00:00.000Z",
installPath: "/tmp/tlon",
version: "2026.2.21",
},
},
},
},
null,
2,
),
"utf-8",
);
const io = createIoForHome(home);
expect(io.loadConfig().plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
expect(io.loadConfig().plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
const snapshot = await io.readConfigFileSnapshot();
expect(snapshot.valid).toBe(true);
expect(
snapshot.legacyIssues.some((issue) => issue.path === "plugins.installs.tlon.spec"),
).toBe(true);
expect(snapshot.config.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
expect(snapshot.config.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
});
});
});