mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
* fix(paths): structurally resolve home dir to prevent Windows path bugs
Extract resolveRawHomeDir as a private function and gate the public
resolveEffectiveHomeDir through a single path.resolve() exit point.
This makes it structurally impossible for unresolved paths (missing
drive letter on Windows) to escape the function, regardless of how
many return paths exist in the raw lookup logic.
Simplify resolveRequiredHomeDir to only resolve the process.cwd()
fallback, since resolveEffectiveHomeDir now returns resolved values.
Fix shortenMeta in tool-meta.ts: the colon-based split for file:line
patterns (e.g. file.txt:12) conflicts with Windows drive letters
(C:\...) because indexOf(":") matches the drive colon first.
shortenHomeInString already handles file:line patterns correctly via
split/join, so the colon split was both unnecessary and harmful.
Update test assertions across all affected files to use path.resolve()
in expected values and input strings so they match the now-correct
resolved output on both Unix and Windows.
Fixes #12119
* fix(changelog): add paths Windows fix entry (#12125)
---------
Co-authored-by: Sebastian <19554889+sebslight@users.noreply.github.com>
230 lines
6.2 KiB
TypeScript
230 lines
6.2 KiB
TypeScript
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
resolveAgentConfig,
|
|
resolveAgentDir,
|
|
resolveAgentModelFallbacksOverride,
|
|
resolveAgentModelPrimary,
|
|
resolveAgentWorkspaceDir,
|
|
} from "./agent-scope.js";
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe("resolveAgentConfig", () => {
|
|
it("should return undefined when no agents config exists", () => {
|
|
const cfg: OpenClawConfig = {};
|
|
const result = resolveAgentConfig(cfg, "main");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("should return undefined when agent id does not exist", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [{ id: "main", workspace: "~/openclaw" }],
|
|
},
|
|
};
|
|
const result = resolveAgentConfig(cfg, "nonexistent");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("should return basic agent config", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "main",
|
|
name: "Main Agent",
|
|
workspace: "~/openclaw",
|
|
agentDir: "~/.openclaw/agents/main",
|
|
model: "anthropic/claude-opus-4",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const result = resolveAgentConfig(cfg, "main");
|
|
expect(result).toEqual({
|
|
name: "Main Agent",
|
|
workspace: "~/openclaw",
|
|
agentDir: "~/.openclaw/agents/main",
|
|
model: "anthropic/claude-opus-4",
|
|
identity: undefined,
|
|
groupChat: undefined,
|
|
subagents: undefined,
|
|
sandbox: undefined,
|
|
tools: undefined,
|
|
});
|
|
});
|
|
|
|
it("supports per-agent model primary+fallbacks", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
defaults: {
|
|
model: {
|
|
primary: "anthropic/claude-sonnet-4",
|
|
fallbacks: ["openai/gpt-4.1"],
|
|
},
|
|
},
|
|
list: [
|
|
{
|
|
id: "linus",
|
|
model: {
|
|
primary: "anthropic/claude-opus-4",
|
|
fallbacks: ["openai/gpt-5.2"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(resolveAgentModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
|
|
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.2"]);
|
|
|
|
// If fallbacks isn't present, we don't override the global fallbacks.
|
|
const cfgNoOverride: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "linus",
|
|
model: {
|
|
primary: "anthropic/claude-opus-4",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(undefined);
|
|
|
|
// Explicit empty list disables global fallbacks for that agent.
|
|
const cfgDisable: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "linus",
|
|
model: {
|
|
primary: "anthropic/claude-opus-4",
|
|
fallbacks: [],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
expect(resolveAgentModelFallbacksOverride(cfgDisable, "linus")).toEqual([]);
|
|
});
|
|
|
|
it("should return agent-specific sandbox config", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "work",
|
|
workspace: "~/openclaw-work",
|
|
sandbox: {
|
|
mode: "all",
|
|
scope: "agent",
|
|
perSession: false,
|
|
workspaceAccess: "ro",
|
|
workspaceRoot: "~/sandboxes",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const result = resolveAgentConfig(cfg, "work");
|
|
expect(result?.sandbox).toEqual({
|
|
mode: "all",
|
|
scope: "agent",
|
|
perSession: false,
|
|
workspaceAccess: "ro",
|
|
workspaceRoot: "~/sandboxes",
|
|
});
|
|
});
|
|
|
|
it("should return agent-specific tools config", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "restricted",
|
|
workspace: "~/openclaw-restricted",
|
|
tools: {
|
|
allow: ["read"],
|
|
deny: ["exec", "write", "edit"],
|
|
elevated: {
|
|
enabled: false,
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const result = resolveAgentConfig(cfg, "restricted");
|
|
expect(result?.tools).toEqual({
|
|
allow: ["read"],
|
|
deny: ["exec", "write", "edit"],
|
|
elevated: {
|
|
enabled: false,
|
|
allowFrom: { whatsapp: ["+15555550123"] },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("should return both sandbox and tools config", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "family",
|
|
workspace: "~/openclaw-family",
|
|
sandbox: {
|
|
mode: "all",
|
|
scope: "agent",
|
|
},
|
|
tools: {
|
|
allow: ["read"],
|
|
deny: ["exec"],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const result = resolveAgentConfig(cfg, "family");
|
|
expect(result?.sandbox?.mode).toBe("all");
|
|
expect(result?.tools?.allow).toEqual(["read"]);
|
|
});
|
|
|
|
it("should normalize agent id", () => {
|
|
const cfg: OpenClawConfig = {
|
|
agents: {
|
|
list: [{ id: "main", workspace: "~/openclaw" }],
|
|
},
|
|
};
|
|
// Should normalize to "main" (default)
|
|
const result = resolveAgentConfig(cfg, "");
|
|
expect(result).toBeDefined();
|
|
expect(result?.workspace).toBe("~/openclaw");
|
|
});
|
|
|
|
// Unix-style paths behave differently on Windows; skip there
|
|
it.skipIf(process.platform === "win32")("uses OPENCLAW_HOME for default agent workspace", () => {
|
|
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
|
|
|
const workspace = resolveAgentWorkspaceDir({} as OpenClawConfig, "main");
|
|
expect(workspace).toBe(path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "workspace"));
|
|
});
|
|
|
|
// Unix-style paths behave differently on Windows; skip there
|
|
it.skipIf(process.platform === "win32")("uses OPENCLAW_HOME for default agentDir", () => {
|
|
vi.stubEnv("OPENCLAW_HOME", "/srv/openclaw-home");
|
|
vi.stubEnv("OPENCLAW_STATE_DIR", "");
|
|
|
|
const agentDir = resolveAgentDir({} as OpenClawConfig, "main");
|
|
expect(agentDir).toBe(
|
|
path.join(path.resolve("/srv/openclaw-home"), ".openclaw", "agents", "main", "agent"),
|
|
);
|
|
});
|
|
});
|