fix(windows): shorten home paths case-insensitively (#121455)

This commit is contained in:
Peter Steinberger
2026-08-10 00:59:32 -07:00
committed by GitHub
parent 7954dde7cc
commit 5b478bb64f
15 changed files with 299 additions and 34 deletions
+1 -1
View File
@@ -1959,7 +1959,7 @@
"test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts",
"test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts",
"test:watch": "node --import tsx scripts/test-projects.mts --watch",
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
"test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts",
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
@@ -1,4 +1,6 @@
// Terminal Core tests cover display-safe path shortening.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { displayString } from "./display-string.js";
@@ -49,6 +51,25 @@ describe("displayString", () => {
const home = path.resolve("test-home", `${pattern}user`);
stubHome(home, "~/state");
expect(displayString(`${home}/state/project`)).toBe("$OPENCLAW_HOME/project");
expect(displayString(path.join(home, "state", "project"))).toBe(
`$OPENCLAW_HOME${path.sep}project`,
);
});
it.skipIf(process.platform !== "win32")(
"shortens real Windows home casing aliases inside table display text",
() => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-home-display-"));
try {
const homeAlias = home.toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
stubHome(home);
expect(displayString(`Workspace: ${homeAlias}\\project`)).toBe("Workspace: ~\\project");
expect(displayString(`İ Workspace: ${homeAlias}\\project`)).toBe("İ Workspace: ~\\project");
} finally {
fs.rmSync(home, { recursive: true, force: true });
}
},
);
});
+20 -2
View File
@@ -1,6 +1,7 @@
// Terminal Core module implements display string behavior.
import os from "node:os";
import path from "node:path";
import { lowercasePreservingWhitespace } from "@openclaw/normalization-core/string-coerce";
// Display-safe string helpers for shortening user home paths.
@@ -73,13 +74,30 @@ function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefine
return explicitHome ? { home, prefix: "$OPENCLAW_HOME" } : { home, prefix: "~" };
}
/** Find a case-insensitive Windows path without changing offsets in the original string. */
function indexOfWindowsPath(input: string, home: string, cursor: number): number {
const foldedHome = lowercasePreservingWhitespace(home);
// Folding the whole display can expand Unicode and shift indices. Fixed-width slices keep
// replacement offsets anchored to the original string while retaining Windows casing rules.
for (let index = cursor; index <= input.length - home.length; index += 1) {
if (lowercasePreservingWhitespace(input.slice(index, index + home.length)) === foldedHome) {
return index;
}
}
return -1;
}
/** Replace a whole-value home or child path without clipping sibling path prefixes. */
function replaceHomePath(input: string, display: { home: string; prefix: string }): string {
let output = "";
let cursor = 0;
// terminal-core is standalone, so it keeps only its token-aware scan local;
// app-level home selection and path rendering remain owned by core.
while (cursor < input.length) {
const index = input.indexOf(display.home, cursor);
const index =
process.platform === "win32"
? indexOfWindowsPath(input, display.home, cursor)
: input.indexOf(display.home, cursor);
if (index < 0) {
return `${output}${input.slice(cursor)}`;
}
+6 -2
View File
@@ -71,6 +71,8 @@ const WINDOWS_USAGE_TEMPLATE_SCOPE_RE =
/^src\/auto-reply\/usage-bar\/template(?:\.windows\.test)?\.ts$/;
const WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE =
/^src\/media-understanding\/attachments\.(?:cache(?:\.test)?|file-url\.windows\.test|normalize(?:\.test)?)\.ts$/;
const WINDOWS_HOME_DISPLAY_SCOPE_RE =
/^(?:src\/(?:utils(?:\.test)?|infra\/(?:home-display|path-guards)|commands\/agents\.commands\.list(?:\.test)?|cli\/daemon-cli\/status\.print(?:\.test)?|agents\/(?:sandbox\/fs-paths|sessions\/tools\/render-utils)(?:\.test)?)|packages\/terminal-core\/src\/display-string(?:\.test)?)\.ts$/;
const CONTROL_UI_I18N_SCOPE_RE =
/^(ui\/src\/i18n\/|ui\/config\/control-ui-locales\.ts$|scripts\/(?:control-ui-i18n(?:-verify)?\.ts|lib\/control-ui-i18n-(?:catalog|config|raw-copy|sync-plan)\.ts)$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
const CONTROL_UI_RAW_COPY_SOURCE_RE = /^ui\/src\/(?:app|components|lib|pages)\/.*\.tsx?$/;
@@ -180,14 +182,16 @@ export function detectChangedScope(changedPaths) {
WINDOWS_SECRETREF_SCOPE_RE.test(path) ||
WINDOWS_DAEMON_SCOPE_RE.test(path) ||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path)) &&
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path)) &&
(!facts.isTestOnly ||
WINDOWS_TEST_SCOPE_RE.test(path) ||
WINDOWS_FILE_URL_SCOPE_RE.test(path) ||
WINDOWS_SECRETREF_TEST_SCOPE_RE.test(path) ||
WINDOWS_DAEMON_SCOPE_RE.test(path) ||
WINDOWS_USAGE_TEMPLATE_SCOPE_RE.test(path) ||
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path))
WINDOWS_MEDIA_UNDERSTANDING_FILE_URL_SCOPE_RE.test(path) ||
WINDOWS_HOME_DISPLAY_SCOPE_RE.test(path))
) {
runWindows = true;
}
+29 -2
View File
@@ -56,7 +56,9 @@ describe("sandbox bind mounts", () => {
});
it("detects bind mounts whose container path differs from the host path", () => {
expect(hasSandboxBindContainerPathAliases(["/tmp/data:/tmp/data:rw"])).toBe(false);
expect(hasSandboxBindContainerPathAliases(["/tmp/data:/tmp/data:rw"])).toBe(
process.platform === "win32",
);
expect(hasSandboxBindContainerPathAliases(["/tmp/data:/data:rw"])).toBe(true);
expect(hasSandboxBindContainerPathAliases(["invalid-bind"])).toBe(false);
});
@@ -181,12 +183,37 @@ describe("resolveSandboxFsPathWithMounts", () => {
expect(thrown).toBeInstanceOf(Error);
const message = (thrown as Error).message;
expect(message).toContain(
"Path escapes sandbox root (~/workspace-coder; container root /workspace): /tmp/outside",
`Path escapes sandbox root (~${path.sep}workspace-coder; container root /workspace): /tmp/outside`,
);
expect(message).toContain("Use a path under /workspace/ instead.");
expect(message).not.toContain(os.homedir());
});
it.skipIf(process.platform !== "win32")(
"does not expose real Windows home casing aliases in escape errors",
() => {
const homeAlias = os.homedir().toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
const workspaceDir = path.join(homeAlias, "workspace-coder");
const sandbox = createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
});
expect(() =>
resolveSandboxFsPathWithMounts({
filePath: "C:\\outside\\secret.txt",
cwd: sandbox.workspaceDir,
defaultWorkspaceRoot: sandbox.workspaceDir,
defaultContainerRoot: sandbox.containerWorkdir,
mounts: buildSandboxFsMounts(sandbox),
}),
).toThrow(
`Path escapes sandbox root (~${path.sep}workspace-coder; container root /workspace)`,
);
},
);
it("prefers custom bind mounts over default workspace mount at /workspace", () => {
const sandbox = createSandbox({
docker: {
+2 -5
View File
@@ -7,6 +7,7 @@ import os from "node:os";
import path from "node:path";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
import { shortenPathWithHome } from "../../infra/home-display.js";
import { isPathInside } from "../../infra/path-guards.js";
import { resolveSandboxInputPath, resolveSandboxPath } from "../sandbox-paths.js";
import type { SandboxFsBridgeContext } from "./backend-handle.types.js";
@@ -315,11 +316,7 @@ function formatSandboxRootEscapeMessage(params: {
}
function shortenHomePath(value: string): string {
const home = os.homedir();
if (value === home || value.startsWith(`${home}${path.sep}`)) {
return `~${value.slice(home.length)}`;
}
return value;
return shortenPathWithHome(value, { home: os.homedir(), prefix: "~" });
}
function compareMountsByContainerPath(a: SandboxFsMount, b: SandboxFsMount): number {
@@ -1,4 +1,6 @@
import fs from "node:fs";
import * as os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { appendSessionToolTruncationWarning, shortenPath } from "./render-utils.js";
@@ -45,6 +47,15 @@ describe("shortenPath", () => {
expect(shortenPath("/var/log/syslog")).toBe("/var/log/syslog");
});
it.skipIf(process.platform !== "win32")("shortens real Windows home casing aliases", () => {
const homeAlias = home.toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
expect(shortenPath(path.join(homeAlias, "projects", "app.ts"))).toBe(
`~${path.sep}projects${path.sep}app.ts`,
);
});
it("returns an empty string for non-string input", () => {
expect(shortenPath(undefined)).toBe("");
});
+2 -8
View File
@@ -5,6 +5,7 @@
*/
import * as os from "node:os";
import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui";
import { shortenPathWithHome } from "../../../infra/home-display.js";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import { sanitizeBinaryOutput } from "../../shell-utils.js";
@@ -16,14 +17,7 @@ export function shortenPath(path: unknown): string {
if (typeof path !== "string") {
return "";
}
const home = os.homedir();
if (path === home) {
return "~";
}
if (path.startsWith(`${home}/`) || path.startsWith(`${home}\\`)) {
return `~${path.slice(home.length)}`;
}
return path;
return shortenPathWithHome(path, { home: os.homedir(), prefix: "~" });
}
/** Returns a display string for string/nullish values, or null for unsupported values. */
+39
View File
@@ -1,5 +1,9 @@
// Daemon status print tests cover user-facing service status formatting.
import fs from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import { withEnv } from "../../test-utils/env.js";
import { formatCliCommand } from "../command-format.js";
import { printDaemonStatus } from "./status.print.js";
@@ -131,6 +135,41 @@ describe("printDaemonStatus", () => {
expectMockLineContains(runtime.log, "8192 MiB constrained memory");
});
it.skipIf(process.platform !== "win32")(
"shortens real Windows home casing aliases in human status",
async () => {
await withTempDir({ prefix: "openclaw-home-display-" }, async (home) => {
const logFile = path.join(home, "logs", "gateway.log");
await fs.promises.mkdir(path.dirname(logFile), { recursive: true });
await fs.promises.writeFile(logFile, "ready", "utf8");
const logFileAlias = logFile.toUpperCase();
expect(fs.statSync(logFileAlias).isFile()).toBe(true);
await withEnv({ OPENCLAW_HOME: home }, async () => {
printDaemonStatus(
{
service: {
label: "Scheduled Task",
loaded: true,
loadedText: "registered",
notLoadedText: "not registered",
},
logFile: logFileAlias,
extraServices: [],
},
{ json: false },
);
});
expectMockLineContains(
runtime.log,
`File logs: $OPENCLAW_HOME${path.sep}LOGS${path.sep}GATEWAY.LOG`,
);
expect(runtime.log.mock.calls.flat().join("\n")).not.toContain(home.toUpperCase());
});
},
);
it("prints stale gateway pid guidance when runtime does not own the listener", () => {
printDaemonStatus(
{
+45 -2
View File
@@ -1,7 +1,11 @@
// Agent command-list tests cover provider metadata and command output for configured agents.
import fs from "node:fs";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { OutputRuntimeEnv } from "../runtime.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { withEnvAsync } from "../test-utils/env.js";
const {
buildProviderStatusIndexMock,
@@ -127,8 +131,8 @@ describe("agentsListCommand", () => {
[
"Agents:",
"- main (default)",
" Workspace: ~/.openclaw/workspace",
" Agent dir: ~/.openclaw/agents/main/agent",
` Workspace: ~${path.sep}.openclaw${path.sep}workspace`,
` Agent dir: ~${path.sep}.openclaw${path.sep}agents${path.sep}main${path.sep}agent`,
" Routing rules: 1",
" Routing: Telegram default",
" Providers:",
@@ -139,4 +143,43 @@ describe("agentsListCommand", () => {
],
]);
});
it.skipIf(process.platform !== "win32")(
"shortens real Windows home casing aliases in human output",
async () => {
await withTempDir({ prefix: "openclaw-home-display-" }, async (home) => {
const workspace = path.join(home, "workspace");
const agentDir = path.join(home, "agents", "main", "agent");
await fs.promises.mkdir(workspace, { recursive: true });
await fs.promises.mkdir(agentDir, { recursive: true });
const homeAlias = home.toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
requireValidConfigMock.mockResolvedValueOnce({
agents: {
list: [
{
id: "main",
default: true,
workspace: path.join(homeAlias, "workspace"),
agentDir: path.join(homeAlias, "agents", "main", "agent"),
},
],
},
} satisfies OpenClawConfig);
const runtime = createRuntime();
await withEnvAsync({ OPENCLAW_HOME: home }, async () => {
await agentsListCommand({}, runtime);
});
const output = vi.mocked(runtime.log).mock.calls.flat().join("\n");
expect(output).toContain(`Workspace: $OPENCLAW_HOME${path.sep}workspace`);
expect(output).toContain(
`Agent dir: $OPENCLAW_HOME${path.sep}agents${path.sep}main${path.sep}agent`,
);
expect(output).not.toContain(homeAlias);
});
},
);
});
+24
View File
@@ -0,0 +1,24 @@
// Shared home-path display policy for core owners with distinct home selection contracts.
import path from "node:path";
import { isPathInside, normalizeWindowsPathPreservingCase } from "./path-guards.js";
/** Replace an absolute home path with its display prefix without clipping sibling paths. */
export function shortenPathWithHome(
input: string,
{ home, prefix }: { home: string; prefix: string },
): string {
if (input === home) {
return prefix;
}
if (input.startsWith(`${home}/`) || input.startsWith(`${home}\\`)) {
return `${prefix}${input.slice(home.length)}`;
}
if (process.platform === "win32" && path.win32.isAbsolute(input) && isPathInside(home, input)) {
const relative = path.win32.relative(
normalizeWindowsPathPreservingCase(home),
normalizeWindowsPathPreservingCase(input),
);
return path.win32.join(prefix, relative);
}
return input;
}
@@ -165,6 +165,30 @@ describe("detectChangedScope Windows routing", () => {
}
});
it("routes shared home display owners and visible command coverage to Windows", () => {
for (const displayPath of [
"src/utils.ts",
"src/utils.test.ts",
"src/infra/home-display.ts",
"src/infra/path-guards.ts",
"src/commands/agents.commands.list.ts",
"src/commands/agents.commands.list.test.ts",
"src/cli/daemon-cli/status.print.ts",
"src/cli/daemon-cli/status.print.test.ts",
"packages/terminal-core/src/display-string.ts",
"packages/terminal-core/src/display-string.test.ts",
"src/agents/sandbox/fs-paths.ts",
"src/agents/sandbox/fs-paths.test.ts",
"src/agents/sessions/tools/render-utils.ts",
"src/agents/sessions/tools/render-utils.test.ts",
]) {
expect(detectChangedScope([displayPath]), displayPath).toMatchObject({
runNode: true,
runWindows: true,
});
}
});
it("routes SecretRef path-security changes and native fixtures to Windows", () => {
for (const secretRefPath of [
"src/commands/doctor-gateway-auth-token.ts",
+57
View File
@@ -197,6 +197,36 @@ describe("shortenHomePath", () => {
);
});
});
it.skipIf(process.platform === "win32")("keeps POSIX home matching case-sensitive", () => {
withEnv({ OPENCLAW_HOME: "/srv/OpenClaw-Home", HOME: "/home/other" }, () => {
expect(shortenHomePath("/srv/openclaw-home/workspace")).toBe("/srv/openclaw-home/workspace");
});
});
it.skipIf(process.platform !== "win32")("keeps relative Windows paths relative", () => {
withEnv({ OPENCLAW_HOME: process.cwd() }, () => {
expect(shortenHomePath(`relative${path.sep}workspace`)).toBe(`relative${path.sep}workspace`);
});
});
it.skipIf(process.platform !== "win32")(
"shortens real extended-length Windows home aliases without exposing the absolute path",
async () => {
await withTempDir({ prefix: "openclaw-home-display-" }, async (home) => {
const workspace = path.join(home, "workspace");
await fs.promises.mkdir(workspace);
const extendedAlias = `\\\\?\\${workspace.toUpperCase()}`;
expect(fs.statSync(extendedAlias).isDirectory()).toBe(true);
withEnv({ OPENCLAW_HOME: home }, () => {
const display = shortenHomePath(extendedAlias);
expect(display).toBe(`$OPENCLAW_HOME${path.sep}WORKSPACE`);
expect(display).not.toContain(home.toUpperCase());
});
});
},
);
});
describe("shortenHomeInString", () => {
@@ -209,6 +239,33 @@ describe("shortenHomeInString", () => {
).toBe("config: $OPENCLAW_HOME/.openclaw/openclaw.json");
});
});
it.skipIf(process.platform === "win32")(
"keeps embedded POSIX home matching case-sensitive",
() => {
withEnv({ OPENCLAW_HOME: "/srv/OpenClaw-Home", HOME: "/home/other" }, () => {
expect(shortenHomeInString("config: /srv/openclaw-home/openclaw.json")).toBe(
"config: /srv/openclaw-home/openclaw.json",
);
});
},
);
it.skipIf(process.platform !== "win32")(
"shortens real Windows home casing aliases inside diagnostic text",
async () => {
await withTempDir({ prefix: "openclaw-home-display-" }, async (home) => {
const homeAlias = home.toUpperCase();
expect(fs.statSync(homeAlias).isDirectory()).toBe(true);
withEnv({ OPENCLAW_HOME: home }, () => {
expect(shortenHomeInString(`config: ${homeAlias}\\openclaw.json`)).toBe(
"config: $OPENCLAW_HOME\\openclaw.json",
);
});
});
},
);
});
describe("resolveUserPath", () => {
+6 -11
View File
@@ -8,7 +8,9 @@ import {
resolveRequiredHomeDir,
resolveUserPath,
} from "./infra/home-dir.js";
import { shortenPathWithHome } from "./infra/home-display.js";
import { isPlainObject } from "./infra/plain-object.js";
import { escapeRegExp as escapeRegExpValue } from "./shared/regexp.js";
export { escapeRegExp } from "./shared/regexp.js";
export { sleep } from "./utils/sleep.js";
export { isRecord } from "@openclaw/normalization-core/record-coerce";
@@ -102,21 +104,11 @@ function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefine
/** Replaces the leading home directory in a path with `~` or `$OPENCLAW_HOME`. */
export function shortenHomePath(input: string): string {
if (!input) {
return input;
}
const display = resolveHomeDisplayPrefix();
if (!display) {
return input;
}
const { home, prefix } = display;
if (input === home) {
return prefix;
}
if (input.startsWith(`${home}/`) || input.startsWith(`${home}\\`)) {
return `${prefix}${input.slice(home.length)}`;
}
return input;
return shortenPathWithHome(input, display);
}
/** Replaces all effective-home occurrences inside a diagnostic string. */
@@ -128,6 +120,9 @@ export function shortenHomeInString(input: string): string {
if (!display) {
return input;
}
if (process.platform === "win32") {
return input.replace(new RegExp(escapeRegExpValue(display.home), "giu"), display.prefix);
}
return input.split(display.home).join(display.prefix);
}
+11
View File
@@ -338,4 +338,15 @@ describe("package scripts", () => {
"src/media-understanding/attachments.file-url.windows.test.ts",
);
});
it("runs shared home display and visible command coverage in Windows CI", () => {
const script = readPackageJson().scripts["test:windows:ci"];
expect(script).toContain("src/utils.test.ts");
expect(script).toContain("src/commands/agents.commands.list.test.ts");
expect(script).toContain("src/cli/daemon-cli/status.print.test.ts");
expect(script).toContain("packages/terminal-core/src/display-string.test.ts");
expect(script).toContain("src/agents/sandbox/fs-paths.test.ts");
expect(script).toContain("src/agents/sessions/tools/render-utils.test.ts");
});
});