refactor: share temp dir test helper

This commit is contained in:
Peter Steinberger 2026-03-13 17:39:32 +00:00
parent b5349f7563
commit 88b87d893d
5 changed files with 90 additions and 95 deletions

View File

@ -1,22 +1,13 @@
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import { import {
buildPinnedWritePlan, buildPinnedWritePlan,
SANDBOX_PINNED_MUTATION_PYTHON, SANDBOX_PINNED_MUTATION_PYTHON,
} from "./fs-bridge-mutation-helper.js"; } from "./fs-bridge-mutation-helper.js";
async function withTempRoot<T>(prefix: string, run: (root: string) => Promise<T>): Promise<T> {
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) { function runMutation(args: string[], input?: string) {
return spawnSync("python3", ["-c", SANDBOX_PINNED_MUTATION_PYTHON, ...args], { return spawnSync("python3", ["-c", SANDBOX_PINNED_MUTATION_PYTHON, ...args], {
input, input,
@ -56,7 +47,7 @@ function runWritePlan(args: string[], input?: string) {
describe("sandbox pinned mutation helper", () => { describe("sandbox pinned mutation helper", () => {
it("writes through a pinned directory fd", async () => { 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"); const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(workspace, { recursive: true });
@ -72,7 +63,7 @@ describe("sandbox pinned mutation helper", () => {
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"preserves stdin payload bytes when the pinned write plan runs through sh", "preserves stdin payload bytes when the pinned write plan runs through sh",
async () => { async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => { await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
const workspace = path.join(root, "workspace"); const workspace = path.join(root, "workspace");
await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(workspace, { recursive: true });
@ -92,7 +83,7 @@ describe("sandbox pinned mutation helper", () => {
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"rejects symlink-parent writes instead of materializing a temp file outside the mount", "rejects symlink-parent writes instead of materializing a temp file outside the mount",
async () => { async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => { await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
const workspace = path.join(root, "workspace"); const workspace = path.join(root, "workspace");
const outside = path.join(root, "outside"); const outside = path.join(root, "outside");
await fs.mkdir(workspace, { recursive: true }); 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 () => { 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 workspace = path.join(root, "workspace");
const outside = path.join(root, "outside"); const outside = path.join(root, "outside");
await fs.mkdir(workspace, { recursive: true }); 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 () => { 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 workspace = path.join(root, "workspace");
const outside = path.join(root, "outside"); const outside = path.join(root, "outside");
await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(workspace, { recursive: true });
@ -144,7 +135,7 @@ describe("sandbox pinned mutation helper", () => {
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"rejects symlink destination parents during rename", "rejects symlink destination parents during rename",
async () => { async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => { await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
const workspace = path.join(root, "workspace"); const workspace = path.join(root, "workspace");
const outside = path.join(root, "outside"); const outside = path.join(root, "outside");
await fs.mkdir(workspace, { recursive: true }); await fs.mkdir(workspace, { recursive: true });
@ -175,7 +166,7 @@ describe("sandbox pinned mutation helper", () => {
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"copies directories across different mount roots during rename fallback", "copies directories across different mount roots during rename fallback",
async () => { async () => {
await withTempRoot("openclaw-mutation-helper-", async (root) => { await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
const sourceRoot = path.join(root, "source"); const sourceRoot = path.join(root, "source");
const destRoot = path.join(root, "dest"); const destRoot = path.join(root, "dest");
await fs.mkdir(path.join(sourceRoot, "dir", "nested"), { recursive: true }); await fs.mkdir(path.join(sourceRoot, "dir", "nested"), { recursive: true });

View File

@ -1,7 +1,7 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { import {
resolveDefaultConfigCandidates, resolveDefaultConfigCandidates,
resolveConfigPathCandidate, resolveConfigPathCandidate,
@ -37,15 +37,6 @@ describe("oauth paths", () => {
}); });
describe("state + config path candidates", () => { describe("state + config path candidates", () => {
async function withTempRoot(prefix: string, run: (root: string) => Promise<void>): Promise<void> {
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 { function expectOpenClawHomeDefaults(env: NodeJS.ProcessEnv): void {
const configuredHome = env.OPENCLAW_HOME; const configuredHome = env.OPENCLAW_HOME;
if (!configuredHome) { if (!configuredHome) {
@ -107,7 +98,7 @@ describe("state + config path candidates", () => {
}); });
it("prefers ~/.openclaw when it exists and legacy dir is missing", async () => { 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"); const newDir = path.join(root, ".openclaw");
await fs.mkdir(newDir, { recursive: true }); await fs.mkdir(newDir, { recursive: true });
const resolved = resolveStateDir({} as NodeJS.ProcessEnv, () => root); 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 () => { 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"); const legacyDir = path.join(root, ".clawdbot");
await fs.mkdir(legacyDir, { recursive: true }); await fs.mkdir(legacyDir, { recursive: true });
const resolved = resolveStateDir({} as NodeJS.ProcessEnv, () => root); 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 () => { 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"); const legacyDir = path.join(root, ".openclaw");
await fs.mkdir(legacyDir, { recursive: true }); await fs.mkdir(legacyDir, { recursive: true });
const legacyPath = path.join(legacyDir, "openclaw.json"); 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 () => { 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"); const legacyDir = path.join(root, ".openclaw");
await fs.mkdir(legacyDir, { recursive: true }); await fs.mkdir(legacyDir, { recursive: true });
const legacyConfig = path.join(legacyDir, "openclaw.json"); const legacyConfig = path.join(legacyDir, "openclaw.json");

View File

@ -1,19 +1,10 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { resolveBoundaryPath, resolveBoundaryPathSync } from "./boundary-path.js"; import { resolveBoundaryPath, resolveBoundaryPathSync } from "./boundary-path.js";
import { isPathInside } from "./path-guards.js"; import { isPathInside } from "./path-guards.js";
async function withTempRoot<T>(prefix: string, run: (root: string) => Promise<T>): Promise<T> {
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 { function createSeededRandom(seed: number): () => number {
let state = seed >>> 0; let state = seed >>> 0;
return () => { return () => {
@ -28,7 +19,7 @@ describe("resolveBoundaryPath", () => {
return; return;
} }
await withTempRoot("openclaw-boundary-path-", async (base) => { await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => {
const root = path.join(base, "workspace"); const root = path.join(base, "workspace");
const targetDir = path.join(root, "target-dir"); const targetDir = path.join(root, "target-dir");
const linkPath = path.join(root, "alias"); const linkPath = path.join(root, "alias");
@ -55,7 +46,7 @@ describe("resolveBoundaryPath", () => {
return; return;
} }
await withTempRoot("openclaw-boundary-path-", async (base) => { await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => {
const root = path.join(base, "workspace"); const root = path.join(base, "workspace");
const outside = path.join(base, "outside"); const outside = path.join(base, "outside");
const linkPath = path.join(root, "alias-out"); const linkPath = path.join(root, "alias-out");
@ -86,7 +77,7 @@ describe("resolveBoundaryPath", () => {
return; return;
} }
await withTempRoot("openclaw-boundary-path-", async (base) => { await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => {
const root = path.join(base, "workspace"); const root = path.join(base, "workspace");
const outside = path.join(base, "outside"); const outside = path.join(base, "outside");
const outsideFile = path.join(outside, "target.txt"); const outsideFile = path.join(outside, "target.txt");
@ -122,7 +113,7 @@ describe("resolveBoundaryPath", () => {
return; return;
} }
await withTempRoot("openclaw-boundary-path-", async (base) => { await withTempDir({ prefix: "openclaw-boundary-path-" }, async (base) => {
const root = path.join(base, "workspace"); const root = path.join(base, "workspace");
const aliasRoot = path.join(base, "workspace-alias"); const aliasRoot = path.join(base, "workspace-alias");
const fileName = "plugin.js"; const fileName = "plugin.js";
@ -153,7 +144,7 @@ describe("resolveBoundaryPath", () => {
return; 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 root = path.join(base, "workspace");
const outside = path.join(base, "outside"); const outside = path.join(base, "outside");
const safeTarget = path.join(root, "safe-target"); const safeTarget = path.join(root, "safe-target");

View File

@ -1,76 +1,75 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { assertNoPathAliasEscape } from "./path-alias-guards.js"; import { assertNoPathAliasEscape } from "./path-alias-guards.js";
async function withTempRoot<T>(run: (root: string) => Promise<T>): Promise<T> {
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", () => { describe("assertNoPathAliasEscape", () => {
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"rejects broken final symlink targets outside root", "rejects broken final symlink targets outside root",
async () => { async () => {
await withTempRoot(async (root) => { await withTempDir(
const outside = path.join(path.dirname(root), "outside"); { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" },
await fs.mkdir(outside, { recursive: true }); async (root) => {
const linkPath = path.join(root, "jump"); const outside = path.join(path.dirname(root), "outside");
await fs.symlink(path.join(outside, "owned.txt"), linkPath); await fs.mkdir(outside, { recursive: true });
const linkPath = path.join(root, "jump");
await fs.symlink(path.join(outside, "owned.txt"), linkPath);
await expect( await expect(
assertNoPathAliasEscape({ assertNoPathAliasEscape({
absolutePath: linkPath, absolutePath: linkPath,
rootPath: root, rootPath: root,
boundaryLabel: "sandbox root", boundaryLabel: "sandbox root",
}), }),
).rejects.toThrow(/Symlink escapes sandbox root/); ).rejects.toThrow(/Symlink escapes sandbox root/);
}); },
);
}, },
); );
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"allows broken final symlink targets that remain inside root", "allows broken final symlink targets that remain inside root",
async () => { async () => {
await withTempRoot(async (root) => { await withTempDir(
const linkPath = path.join(root, "jump"); { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" },
await fs.symlink(path.join(root, "missing", "owned.txt"), linkPath); async (root) => {
const linkPath = path.join(root, "jump");
await fs.symlink(path.join(root, "missing", "owned.txt"), linkPath);
await expect( await expect(
assertNoPathAliasEscape({ assertNoPathAliasEscape({
absolutePath: linkPath, absolutePath: linkPath,
rootPath: root, rootPath: root,
boundaryLabel: "sandbox root", boundaryLabel: "sandbox root",
}), }),
).resolves.toBeUndefined(); ).resolves.toBeUndefined();
}); },
);
}, },
); );
it.runIf(process.platform !== "win32")( it.runIf(process.platform !== "win32")(
"rejects broken targets that traverse via an in-root symlink alias", "rejects broken targets that traverse via an in-root symlink alias",
async () => { async () => {
await withTempRoot(async (root) => { await withTempDir(
const outside = path.join(path.dirname(root), "outside"); { prefix: "openclaw-path-alias-", parentDir: process.cwd(), subdir: "root" },
await fs.mkdir(outside, { recursive: true }); async (root) => {
await fs.symlink(outside, path.join(root, "hop")); const outside = path.join(path.dirname(root), "outside");
const linkPath = path.join(root, "jump"); await fs.mkdir(outside, { recursive: true });
await fs.symlink(path.join("hop", "missing", "owned.txt"), linkPath); 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( await expect(
assertNoPathAliasEscape({ assertNoPathAliasEscape({
absolutePath: linkPath, absolutePath: linkPath,
rootPath: root, rootPath: root,
boundaryLabel: "sandbox root", boundaryLabel: "sandbox root",
}), }),
).rejects.toThrow(/Symlink escapes sandbox root/); ).rejects.toThrow(/Symlink escapes sandbox root/);
}); },
);
}, },
); );
}); });

View File

@ -0,0 +1,23 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
export async function withTempDir<T>(
options: {
prefix: string;
parentDir?: string;
subdir?: string;
},
run: (dir: string) => Promise<T>,
): Promise<T> {
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 });
}
}