From eb4a93a8dbdf51918af3198155b8445e2dde1e19 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 25 Feb 2026 01:59:43 +0000 Subject: [PATCH] refactor(sandbox): share container-path utils and tighten fs bridge tests --- docs/reference/test.md | 13 +++++++++++++ src/agents/sandbox/fs-bridge.test.ts | 12 +++++++++--- src/agents/sandbox/fs-bridge.ts | 15 ++------------- src/agents/sandbox/fs-paths.ts | 16 ++-------------- src/agents/sandbox/path-utils.ts | 15 +++++++++++++++ 5 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 src/agents/sandbox/path-utils.ts diff --git a/docs/reference/test.md b/docs/reference/test.md index 91db2244bd0..e369b4da7ad 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -15,6 +15,19 @@ title: "Tests" - `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `vmForks` + adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs. - `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip. +## Local PR gate + +For local PR land/gate checks, run: + +- `pnpm check` +- `pnpm build` +- `pnpm test` +- `pnpm check:docs` + +If `pnpm test` flakes on a loaded host, rerun once before treating it as a regression, then isolate with `pnpm vitest run `. For memory-constrained hosts, use: + +- `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test` + ## Model latency bench (local keys) Script: [`scripts/bench-model.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/bench-model.ts) diff --git a/src/agents/sandbox/fs-bridge.test.ts b/src/agents/sandbox/fs-bridge.test.ts index aa3144ca331..d3bcd735e9e 100644 --- a/src/agents/sandbox/fs-bridge.test.ts +++ b/src/agents/sandbox/fs-bridge.test.ts @@ -13,13 +13,19 @@ import { createSandboxTestContext } from "./test-fixtures.js"; import type { SandboxContext } from "./types.js"; const mockedExecDockerRaw = vi.mocked(execDockerRaw); +const DOCKER_SCRIPT_INDEX = 5; +const DOCKER_FIRST_SCRIPT_ARG_INDEX = 7; function getDockerScript(args: string[]): string { - return String(args[5] ?? ""); + return String(args[DOCKER_SCRIPT_INDEX] ?? ""); +} + +function getDockerArg(args: string[], position: number): string { + return String(args[DOCKER_FIRST_SCRIPT_ARG_INDEX + position - 1] ?? ""); } function getDockerPathArg(args: string[]): string { - return String(args.at(-1) ?? ""); + return getDockerArg(args, 1); } function getScriptsFromCalls(): string[] { @@ -50,7 +56,7 @@ describe("sandbox fs bridge shell compatibility", () => { const script = getDockerScript(args); if (script.includes('readlink -f -- "$cursor"')) { return { - stdout: Buffer.from(`${String(args.at(-2) ?? "")}\n`), + stdout: Buffer.from(`${getDockerArg(args, 1)}\n`), stderr: Buffer.alloc(0), code: 0, }; diff --git a/src/agents/sandbox/fs-bridge.ts b/src/agents/sandbox/fs-bridge.ts index dee44e1b237..226fc39ca1d 100644 --- a/src/agents/sandbox/fs-bridge.ts +++ b/src/agents/sandbox/fs-bridge.ts @@ -8,6 +8,7 @@ import { type SandboxResolvedFsPath, type SandboxFsMount, } from "./fs-paths.js"; +import { isPathInsideContainerRoot, normalizeContainerPath } from "./path-utils.js"; import type { SandboxContext, SandboxWorkspaceAccess } from "./types.js"; type RunCommandOptions = { @@ -277,7 +278,7 @@ class SandboxFsBridgeImpl implements SandboxFsBridge { private resolveMountByContainerPath(containerPath: string): SandboxFsMount | null { const normalized = normalizeContainerPath(containerPath); for (const mount of this.mountsByContainer) { - if (isPathInsidePosix(normalizeContainerPath(mount.containerRoot), normalized)) { + if (isPathInsideContainerRoot(normalizeContainerPath(mount.containerRoot), normalized)) { return mount; } } @@ -351,18 +352,6 @@ function coerceStatType(typeRaw?: string): "file" | "directory" | "other" { return "other"; } -function normalizeContainerPath(value: string): string { - const normalized = path.posix.normalize(value); - return normalized === "." ? "/" : normalized; -} - -function isPathInsidePosix(root: string, target: string): boolean { - if (root === "/") { - return true; - } - return target === root || target.startsWith(`${root}/`); -} - async function assertNoHostSymlinkEscape(params: { absolutePath: string; rootPath: string; diff --git a/src/agents/sandbox/fs-paths.ts b/src/agents/sandbox/fs-paths.ts index 3073c6e8458..7cd239ce0f3 100644 --- a/src/agents/sandbox/fs-paths.ts +++ b/src/agents/sandbox/fs-paths.ts @@ -3,6 +3,7 @@ import { resolveSandboxInputPath, resolveSandboxPath } from "../sandbox-paths.js import { splitSandboxBindSpec } from "./bind-spec.js"; import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { resolveSandboxHostPathViaExistingAncestor } from "./host-paths.js"; +import { isPathInsideContainerRoot, normalizeContainerPath } from "./path-utils.js"; import type { SandboxContext } from "./types.js"; export type SandboxFsMount = { @@ -201,7 +202,7 @@ function dedupeMounts(mounts: SandboxFsMount[]): SandboxFsMount[] { function findMountByContainerPath(mounts: SandboxFsMount[], target: string): SandboxFsMount | null { for (const mount of mounts) { - if (isPathInsidePosix(mount.containerRoot, target)) { + if (isPathInsideContainerRoot(mount.containerRoot, target)) { return mount; } } @@ -217,14 +218,6 @@ function findMountByHostPath(mounts: SandboxFsMount[], target: string): SandboxF return null; } -function isPathInsidePosix(root: string, target: string): boolean { - const rel = path.posix.relative(root, target); - if (!rel) { - return true; - } - return !(rel.startsWith("..") || path.posix.isAbsolute(rel)); -} - function isPathInsideHost(root: string, target: string): boolean { const canonicalRoot = resolveSandboxHostPathViaExistingAncestor(path.resolve(root)); const resolvedTarget = path.resolve(target); @@ -259,11 +252,6 @@ function toDisplayRelative(params: { return params.containerPath; } -function normalizeContainerPath(value: string): string { - const normalized = path.posix.normalize(value); - return normalized === "." ? "/" : normalized; -} - function normalizePosixInput(value: string): string { return value.replace(/\\/g, "/").trim(); } diff --git a/src/agents/sandbox/path-utils.ts b/src/agents/sandbox/path-utils.ts new file mode 100644 index 00000000000..7bbc840fef1 --- /dev/null +++ b/src/agents/sandbox/path-utils.ts @@ -0,0 +1,15 @@ +import path from "node:path"; + +export function normalizeContainerPath(value: string): string { + const normalized = path.posix.normalize(value); + return normalized === "." ? "/" : normalized; +} + +export function isPathInsideContainerRoot(root: string, target: string): boolean { + const normalizedRoot = normalizeContainerPath(root); + const normalizedTarget = normalizeContainerPath(target); + if (normalizedRoot === "/") { + return true; + } + return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}/`); +}