openclaw/src/config/paths.test.ts
Seb Slight db137dd65d
fix(paths): respect OPENCLAW_HOME for all internal path resolution (#12091)
* fix(paths): respect OPENCLAW_HOME for all internal path resolution (#11995)

Add home-dir module (src/infra/home-dir.ts) that centralizes home
directory resolution with precedence: OPENCLAW_HOME > HOME > USERPROFILE > os.homedir().

Migrate all path-sensitive callsites: config IO, agent dirs, session
transcripts, pairing store, cron store, doctor, CLI profiles.

Add envHomedir() helper in config/paths.ts to reduce lambda noise.
Document OPENCLAW_HOME in docs/help/environment.md.

* fix(paths): handle OPENCLAW_HOME '~' fallback (#12091) (thanks @sebslight)

* docs: mention OPENCLAW_HOME in install and getting started (#12091) (thanks @sebslight)

* fix(status): show OPENCLAW_HOME in shortened paths (#12091) (thanks @sebslight)

* docs(changelog): clarify OPENCLAW_HOME and HOME precedence (#12091) (thanks @sebslight)
2026-02-08 16:20:13 -05:00

196 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 {
resolveDefaultConfigCandidates,
resolveConfigPath,
resolveOAuthDir,
resolveOAuthPath,
resolveStateDir,
} from "./paths.js";
describe("oauth paths", () => {
it("prefers OPENCLAW_OAUTH_DIR over OPENCLAW_STATE_DIR", () => {
const env = {
OPENCLAW_OAUTH_DIR: "/custom/oauth",
OPENCLAW_STATE_DIR: "/custom/state",
} as NodeJS.ProcessEnv;
expect(resolveOAuthDir(env, "/custom/state")).toBe(path.resolve("/custom/oauth"));
expect(resolveOAuthPath(env, "/custom/state")).toBe(
path.join(path.resolve("/custom/oauth"), "oauth.json"),
);
});
it("derives oauth path from OPENCLAW_STATE_DIR when unset", () => {
const env = {
OPENCLAW_STATE_DIR: "/custom/state",
} as NodeJS.ProcessEnv;
expect(resolveOAuthDir(env, "/custom/state")).toBe(path.join("/custom/state", "credentials"));
expect(resolveOAuthPath(env, "/custom/state")).toBe(
path.join("/custom/state", "credentials", "oauth.json"),
);
});
});
describe("state + config path candidates", () => {
it("uses OPENCLAW_STATE_DIR when set", () => {
const env = {
OPENCLAW_STATE_DIR: "/new/state",
} as NodeJS.ProcessEnv;
expect(resolveStateDir(env, () => "/home/test")).toBe(path.resolve("/new/state"));
});
it("uses OPENCLAW_HOME for default state/config locations", () => {
const env = {
OPENCLAW_HOME: "/srv/openclaw-home",
} as NodeJS.ProcessEnv;
expect(resolveStateDir(env)).toBe(path.join("/srv/openclaw-home", ".openclaw"));
const candidates = resolveDefaultConfigCandidates(env);
expect(candidates[0]).toBe(path.join("/srv/openclaw-home", ".openclaw", "openclaw.json"));
});
it("prefers OPENCLAW_HOME over HOME for default state/config locations", () => {
const env = {
OPENCLAW_HOME: "/srv/openclaw-home",
HOME: "/home/other",
} as NodeJS.ProcessEnv;
expect(resolveStateDir(env)).toBe(path.join("/srv/openclaw-home", ".openclaw"));
const candidates = resolveDefaultConfigCandidates(env);
expect(candidates[0]).toBe(path.join("/srv/openclaw-home", ".openclaw", "openclaw.json"));
});
it("orders default config candidates in a stable order", () => {
const home = "/home/test";
const candidates = resolveDefaultConfigCandidates({} as NodeJS.ProcessEnv, () => home);
const expected = [
path.join(home, ".openclaw", "openclaw.json"),
path.join(home, ".openclaw", "clawdbot.json"),
path.join(home, ".openclaw", "moltbot.json"),
path.join(home, ".openclaw", "moldbot.json"),
path.join(home, ".clawdbot", "openclaw.json"),
path.join(home, ".clawdbot", "clawdbot.json"),
path.join(home, ".clawdbot", "moltbot.json"),
path.join(home, ".clawdbot", "moldbot.json"),
path.join(home, ".moltbot", "openclaw.json"),
path.join(home, ".moltbot", "clawdbot.json"),
path.join(home, ".moltbot", "moltbot.json"),
path.join(home, ".moltbot", "moldbot.json"),
path.join(home, ".moldbot", "openclaw.json"),
path.join(home, ".moldbot", "clawdbot.json"),
path.join(home, ".moldbot", "moltbot.json"),
path.join(home, ".moldbot", "moldbot.json"),
];
expect(candidates).toEqual(expected);
});
it("prefers ~/.openclaw when it exists and legacy dir is missing", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-state-"));
try {
const newDir = path.join(root, ".openclaw");
await fs.mkdir(newDir, { recursive: true });
const resolved = resolveStateDir({} as NodeJS.ProcessEnv, () => root);
expect(resolved).toBe(newDir);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("CONFIG_PATH prefers existing config when present", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
const previousHomeDrive = process.env.HOMEDRIVE;
const previousHomePath = process.env.HOMEPATH;
const previousOpenClawConfig = process.env.OPENCLAW_CONFIG_PATH;
const previousOpenClawState = process.env.OPENCLAW_STATE_DIR;
try {
const legacyDir = path.join(root, ".openclaw");
await fs.mkdir(legacyDir, { recursive: true });
const legacyPath = path.join(legacyDir, "openclaw.json");
await fs.writeFile(legacyPath, "{}", "utf-8");
process.env.HOME = root;
if (process.platform === "win32") {
process.env.USERPROFILE = root;
const parsed = path.win32.parse(root);
process.env.HOMEDRIVE = parsed.root.replace(/\\$/, "");
process.env.HOMEPATH = root.slice(parsed.root.length - 1);
}
delete process.env.OPENCLAW_CONFIG_PATH;
delete process.env.OPENCLAW_STATE_DIR;
vi.resetModules();
const { CONFIG_PATH } = await import("./paths.js");
expect(CONFIG_PATH).toBe(legacyPath);
} finally {
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
if (previousUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = previousUserProfile;
}
if (previousHomeDrive === undefined) {
delete process.env.HOMEDRIVE;
} else {
process.env.HOMEDRIVE = previousHomeDrive;
}
if (previousHomePath === undefined) {
delete process.env.HOMEPATH;
} else {
process.env.HOMEPATH = previousHomePath;
}
if (previousOpenClawConfig === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
}
if (previousOpenClawConfig === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = previousOpenClawConfig;
}
if (previousOpenClawState === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
}
if (previousOpenClawState === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousOpenClawState;
}
await fs.rm(root, { recursive: true, force: true });
vi.resetModules();
}
});
it("respects state dir overrides when config is missing", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-override-"));
try {
const legacyDir = path.join(root, ".openclaw");
await fs.mkdir(legacyDir, { recursive: true });
const legacyConfig = path.join(legacyDir, "openclaw.json");
await fs.writeFile(legacyConfig, "{}", "utf-8");
const overrideDir = path.join(root, "override");
const env = { OPENCLAW_STATE_DIR: overrideDir } as NodeJS.ProcessEnv;
const resolved = resolveConfigPath(env, overrideDir, () => root);
expect(resolved).toBe(path.join(overrideDir, "openclaw.json"));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
});