mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(windows): shorten home paths case-insensitively (#121455)
This commit is contained in:
committed by
GitHub
parent
7954dde7cc
commit
5b478bb64f
+1
-1
@@ -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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user