From 88b87d893d5cfbf11e97f6f17489f446a9355cc1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 17:39:32 +0000 Subject: [PATCH] refactor: share temp dir test helper --- .../sandbox/fs-bridge-mutation-helper.test.ts | 25 ++--- src/config/paths.test.ts | 19 +--- src/infra/boundary-path.test.ts | 21 ++-- src/infra/path-alias-guards.test.ts | 97 +++++++++---------- src/test-helpers/temp-dir.ts | 23 +++++ 5 files changed, 90 insertions(+), 95 deletions(-) create mode 100644 src/test-helpers/temp-dir.ts diff --git a/src/agents/sandbox/fs-bridge-mutation-helper.test.ts b/src/agents/sandbox/fs-bridge-mutation-helper.test.ts index 57f22cc84b6..973c81341d1 100644 --- a/src/agents/sandbox/fs-bridge-mutation-helper.test.ts +++ b/src/agents/sandbox/fs-bridge-mutation-helper.test.ts @@ -1,22 +1,13 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { withTempDir } from "../../test-helpers/temp-dir.js"; import { buildPinnedWritePlan, SANDBOX_PINNED_MUTATION_PYTHON, } from "./fs-bridge-mutation-helper.js"; -async function withTempRoot(prefix: string, run: (root: string) => Promise): Promise { - const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); - try { - return await run(root); - } finally { - await fs.rm(root, { recursive: true, force: true }); - } -} - function runMutation(args: string[], input?: string) { return spawnSync("python3", ["-c", SANDBOX_PINNED_MUTATION_PYTHON, ...args], { input, @@ -56,7 +47,7 @@ function runWritePlan(args: string[], input?: string) { describe("sandbox pinned mutation helper", () => { it("writes through a pinned directory fd", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); await fs.mkdir(workspace, { recursive: true }); @@ -72,7 +63,7 @@ describe("sandbox pinned mutation helper", () => { it.runIf(process.platform !== "win32")( "preserves stdin payload bytes when the pinned write plan runs through sh", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); await fs.mkdir(workspace, { recursive: true }); @@ -92,7 +83,7 @@ describe("sandbox pinned mutation helper", () => { it.runIf(process.platform !== "win32")( "rejects symlink-parent writes instead of materializing a temp file outside the mount", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); const outside = path.join(root, "outside"); await fs.mkdir(workspace, { recursive: true }); @@ -108,7 +99,7 @@ describe("sandbox pinned mutation helper", () => { ); it.runIf(process.platform !== "win32")("rejects symlink segments during mkdirp", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); const outside = path.join(root, "outside"); await fs.mkdir(workspace, { recursive: true }); @@ -123,7 +114,7 @@ describe("sandbox pinned mutation helper", () => { }); it.runIf(process.platform !== "win32")("remove unlinks the symlink itself", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); const outside = path.join(root, "outside"); await fs.mkdir(workspace, { recursive: true }); @@ -144,7 +135,7 @@ describe("sandbox pinned mutation helper", () => { it.runIf(process.platform !== "win32")( "rejects symlink destination parents during rename", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const workspace = path.join(root, "workspace"); const outside = path.join(root, "outside"); await fs.mkdir(workspace, { recursive: true }); @@ -175,7 +166,7 @@ describe("sandbox pinned mutation helper", () => { it.runIf(process.platform !== "win32")( "copies directories across different mount roots during rename fallback", async () => { - await withTempRoot("openclaw-mutation-helper-", async (root) => { + await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => { const sourceRoot = path.join(root, "source"); const destRoot = path.join(root, "dest"); await fs.mkdir(path.join(sourceRoot, "dir", "nested"), { recursive: true }); diff --git a/src/config/paths.test.ts b/src/config/paths.test.ts index b8afe7674cb..6d2ffcfaf08 100644 --- a/src/config/paths.test.ts +++ b/src/config/paths.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { withTempDir } from "../test-helpers/temp-dir.js"; import { resolveDefaultConfigCandidates, resolveConfigPathCandidate, @@ -37,15 +37,6 @@ describe("oauth paths", () => { }); describe("state + config path candidates", () => { - async function withTempRoot(prefix: string, run: (root: string) => Promise): Promise { - const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); - try { - await run(root); - } finally { - await fs.rm(root, { recursive: true, force: true }); - } - } - function expectOpenClawHomeDefaults(env: NodeJS.ProcessEnv): void { const configuredHome = env.OPENCLAW_HOME; if (!configuredHome) { @@ -107,7 +98,7 @@ describe("state + config path candidates", () => { }); it("prefers ~/.openclaw when it exists and legacy dir is missing", async () => { - await withTempRoot("openclaw-state-", async (root) => { + await withTempDir({ prefix: "openclaw-state-" }, async (root) => { const newDir = path.join(root, ".openclaw"); await fs.mkdir(newDir, { recursive: true }); const resolved = resolveStateDir({} as NodeJS.ProcessEnv, () => root); @@ -116,7 +107,7 @@ describe("state + config path candidates", () => { }); it("falls back to existing legacy state dir when ~/.openclaw is missing", async () => { - await withTempRoot("openclaw-state-legacy-", async (root) => { + await withTempDir({ prefix: "openclaw-state-legacy-" }, async (root) => { const legacyDir = path.join(root, ".clawdbot"); await fs.mkdir(legacyDir, { recursive: true }); const resolved = resolveStateDir({} as NodeJS.ProcessEnv, () => root); @@ -125,7 +116,7 @@ describe("state + config path candidates", () => { }); it("CONFIG_PATH prefers existing config when present", async () => { - await withTempRoot("openclaw-config-", async (root) => { + await withTempDir({ prefix: "openclaw-config-" }, async (root) => { const legacyDir = path.join(root, ".openclaw"); await fs.mkdir(legacyDir, { recursive: true }); const legacyPath = path.join(legacyDir, "openclaw.json"); @@ -137,7 +128,7 @@ describe("state + config path candidates", () => { }); it("respects state dir overrides when config is missing", async () => { - await withTempRoot("openclaw-config-override-", async (root) => { + await withTempDir({ prefix: "openclaw-config-override-" }, async (root) => { const legacyDir = path.join(root, ".openclaw"); await fs.mkdir(legacyDir, { recursive: true }); const legacyConfig = path.join(legacyDir, "openclaw.json"); diff --git a/src/infra/boundary-path.test.ts b/src/infra/boundary-path.test.ts index d28bb6cdffa..bf7b20ffcc0 100644 --- a/src/infra/boundary-path.test.ts +++ b/src/infra/boundary-path.test.ts @@ -1,19 +1,10 @@ import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { withTempDir } from "../test-helpers/temp-dir.js"; import { resolveBoundaryPath, resolveBoundaryPathSync } from "./boundary-path.js"; import { isPathInside } from "./path-guards.js"; -async function withTempRoot(prefix: string, run: (root: string) => Promise): Promise { - const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); - try { - return await run(root); - } finally { - await fs.rm(root, { recursive: true, force: true }); - } -} - function createSeededRandom(seed: number): () => number { let state = seed >>> 0; return () => { @@ -28,7 +19,7 @@ describe("resolveBoundaryPath", () => { return; } - await withTempRoot("openclaw-boundary-path-", async (base) => { + await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => { const root = path.join(base, "workspace"); const targetDir = path.join(root, "target-dir"); const linkPath = path.join(root, "alias"); @@ -55,7 +46,7 @@ describe("resolveBoundaryPath", () => { return; } - await withTempRoot("openclaw-boundary-path-", async (base) => { + await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => { const root = path.join(base, "workspace"); const outside = path.join(base, "outside"); const linkPath = path.join(root, "alias-out"); @@ -86,7 +77,7 @@ describe("resolveBoundaryPath", () => { return; } - await withTempRoot("openclaw-boundary-path-", async (base) => { + await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => { const root = path.join(base, "workspace"); const outside = path.join(base, "outside"); const outsideFile = path.join(outside, "target.txt"); @@ -122,7 +113,7 @@ describe("resolveBoundaryPath", () => { return; } - await withTempRoot("openclaw-boundary-path-", async (base) => { + await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => { const root = path.join(base, "workspace"); const aliasRoot = path.join(base, "workspace-alias"); const fileName = "plugin.js"; @@ -153,7 +144,7 @@ describe("resolveBoundaryPath", () => { return; } - await withTempRoot("openclaw-boundary-path-fuzz-", async (base) => { + await withTempDir({ prefix: "openclaw-boundary-path-fuzz-" }, async (base) => { const root = path.join(base, "workspace"); const outside = path.join(base, "outside"); const safeTarget = path.join(root, "safe-target"); diff --git a/src/infra/path-alias-guards.test.ts b/src/infra/path-alias-guards.test.ts index abc16c48847..7d70b79805a 100644 --- a/src/infra/path-alias-guards.test.ts +++ b/src/infra/path-alias-guards.test.ts @@ -1,76 +1,75 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { withTempDir } from "../test-helpers/temp-dir.js"; import { assertNoPathAliasEscape } from "./path-alias-guards.js"; -async function withTempRoot(run: (root: string) => Promise): Promise { - const base = await fs.mkdtemp(path.join(process.cwd(), "openclaw-path-alias-")); - const root = path.join(base, "root"); - await fs.mkdir(root, { recursive: true }); - try { - return await run(root); - } finally { - await fs.rm(base, { recursive: true, force: true }); - } -} - describe("assertNoPathAliasEscape", () => { it.runIf(process.platform !== "win32")( "rejects broken final symlink targets outside root", async () => { - await withTempRoot(async (root) => { - const outside = path.join(path.dirname(root), "outside"); - await fs.mkdir(outside, { recursive: true }); - const linkPath = path.join(root, "jump"); - await fs.symlink(path.join(outside, "owned.txt"), linkPath); + await withTempDir( + { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" }, + async (root) => { + const outside = path.join(path.dirname(root), "outside"); + await fs.mkdir(outside, { recursive: true }); + const linkPath = path.join(root, "jump"); + await fs.symlink(path.join(outside, "owned.txt"), linkPath); - await expect( - assertNoPathAliasEscape({ - absolutePath: linkPath, - rootPath: root, - boundaryLabel: "sandbox root", - }), - ).rejects.toThrow(/Symlink escapes sandbox root/); - }); + await expect( + assertNoPathAliasEscape({ + absolutePath: linkPath, + rootPath: root, + boundaryLabel: "sandbox root", + }), + ).rejects.toThrow(/Symlink escapes sandbox root/); + }, + ); }, ); it.runIf(process.platform !== "win32")( "allows broken final symlink targets that remain inside root", async () => { - await withTempRoot(async (root) => { - const linkPath = path.join(root, "jump"); - await fs.symlink(path.join(root, "missing", "owned.txt"), linkPath); + await withTempDir( + { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" }, + async (root) => { + const linkPath = path.join(root, "jump"); + await fs.symlink(path.join(root, "missing", "owned.txt"), linkPath); - await expect( - assertNoPathAliasEscape({ - absolutePath: linkPath, - rootPath: root, - boundaryLabel: "sandbox root", - }), - ).resolves.toBeUndefined(); - }); + await expect( + assertNoPathAliasEscape({ + absolutePath: linkPath, + rootPath: root, + boundaryLabel: "sandbox root", + }), + ).resolves.toBeUndefined(); + }, + ); }, ); it.runIf(process.platform !== "win32")( "rejects broken targets that traverse via an in-root symlink alias", async () => { - await withTempRoot(async (root) => { - const outside = path.join(path.dirname(root), "outside"); - await fs.mkdir(outside, { recursive: true }); - await fs.symlink(outside, path.join(root, "hop")); - const linkPath = path.join(root, "jump"); - await fs.symlink(path.join("hop", "missing", "owned.txt"), linkPath); + await withTempDir( + { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" }, + async (root) => { + const outside = path.join(path.dirname(root), "outside"); + await fs.mkdir(outside, { recursive: true }); + await fs.symlink(outside, path.join(root, "hop")); + const linkPath = path.join(root, "jump"); + await fs.symlink(path.join("hop", "missing", "owned.txt"), linkPath); - await expect( - assertNoPathAliasEscape({ - absolutePath: linkPath, - rootPath: root, - boundaryLabel: "sandbox root", - }), - ).rejects.toThrow(/Symlink escapes sandbox root/); - }); + await expect( + assertNoPathAliasEscape({ + absolutePath: linkPath, + rootPath: root, + boundaryLabel: "sandbox root", + }), + ).rejects.toThrow(/Symlink escapes sandbox root/); + }, + ); }, ); }); diff --git a/src/test-helpers/temp-dir.ts b/src/test-helpers/temp-dir.ts new file mode 100644 index 00000000000..b5a55dfe03d --- /dev/null +++ b/src/test-helpers/temp-dir.ts @@ -0,0 +1,23 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +export async function withTempDir( + options: { + prefix: string; + parentDir?: string; + subdir?: string; + }, + run: (dir: string) => Promise, +): Promise { + const base = await fs.mkdtemp(path.join(options.parentDir ?? os.tmpdir(), options.prefix)); + const dir = options.subdir ? path.join(base, options.subdir) : base; + if (options.subdir) { + await fs.mkdir(dir, { recursive: true }); + } + try { + return await run(dir); + } finally { + await fs.rm(base, { recursive: true, force: true }); + } +}