mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
216 lines
7.2 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|