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 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<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) {
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 });

View File

@ -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<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 {
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");

View File

@ -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<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 {
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");

View File

@ -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<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", () => {
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/);
},
);
},
);
});

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 });
}
}