Skip to content

Commit 4b26b1d

Browse files
test: make fs-safe symlink tests compatible with Windows
1 parent d17b970 commit 4b26b1d

1 file changed

Lines changed: 41 additions & 9 deletions

File tree

src/infra/fs-safe.test.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,40 @@
11
// Tests safe filesystem wrappers and protected file-handle behavior.
22
import type { FileHandle } from "node:fs/promises";
33
import fs from "node:fs/promises";
4+
import fsSync from "node:fs";
5+
import os from "node:os";
46
import path from "node:path";
57
import { __setFsSafeTestHooksForTest } from "@openclaw/fs-safe/test-hooks";
8+
9+
const canCreateFileSymlinks = (() => {
10+
const probeDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-fs-file-symlink-probe-"));
11+
const targetFile = path.join(probeDir, "target.txt");
12+
const linkFile = path.join(probeDir, "link.txt");
13+
try {
14+
fsSync.writeFileSync(targetFile, "target", "utf8");
15+
fsSync.symlinkSync(targetFile, linkFile, "file");
16+
return true;
17+
} catch {
18+
return false;
19+
} finally {
20+
fsSync.rmSync(probeDir, { recursive: true, force: true });
21+
}
22+
})();
23+
24+
const canCreateDirectorySymlinks = (() => {
25+
const probeDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-fs-dir-symlink-probe-"));
26+
const targetDir = path.join(probeDir, "target");
27+
const linkDir = path.join(probeDir, "link");
28+
try {
29+
fsSync.mkdirSync(targetDir);
30+
fsSync.symlinkSync(targetDir, linkDir, process.platform === "win32" ? "junction" : "dir");
31+
return true;
32+
} catch {
33+
return false;
34+
} finally {
35+
fsSync.rmSync(probeDir, { recursive: true, force: true });
36+
}
37+
})();
638
import { afterEach, describe, expect, it, vi } from "vitest";
739
import { withEnv, withEnvAsync } from "../test-utils/env.js";
840
import {
@@ -163,7 +195,7 @@ describe("fs-safe", () => {
163195
await expectRejectCode(readLocalFileSafely({ filePath: file, maxBytes: 4 }), "too-large");
164196
});
165197

166-
it.runIf(process.platform !== "win32")("rejects symlinks", async () => {
198+
it.skipIf(!canCreateFileSymlinks)("rejects symlinks", async () => {
167199
const dir = await tempDirs.make("openclaw-fs-safe-");
168200
const target = path.join(dir, "target.txt");
169201
const link = path.join(dir, "link.txt");
@@ -280,7 +312,7 @@ describe("fs-safe", () => {
280312
await expect(readScoped(scopedPath)).resolves.toEqual(Buffer.from("scoped"));
281313
});
282314

283-
it.runIf(process.platform !== "win32")("blocks symlink escapes under root", async () => {
315+
it.skipIf(!canCreateFileSymlinks)("blocks symlink escapes under root", async () => {
284316
const root = await tempDirs.make("openclaw-fs-safe-root-");
285317
const outside = await tempDirs.make("openclaw-fs-safe-outside-");
286318
const target = path.join(outside, "outside.txt");
@@ -291,7 +323,7 @@ describe("fs-safe", () => {
291323
await expectRejectCode((await openRoot(root)).open("link.txt"), "symlink");
292324
});
293325

294-
it.runIf(process.platform !== "win32")(
326+
it.skipIf(!canCreateFileSymlinks)(
295327
"rejects symlink-target reads when the path target changes after open",
296328
async () => {
297329
const root = await tempDirs.make("openclaw-fs-safe-root-");
@@ -413,14 +445,14 @@ describe("fs-safe", () => {
413445
expect(stat.isDirectory()).toBe(true);
414446
});
415447

416-
it.runIf(process.platform !== "win32")(
448+
it.skipIf(!canCreateDirectorySymlinks)(
417449
"creates directories through in-root symlink parents",
418450
async () => {
419451
const root = await tempDirs.make("openclaw-fs-safe-root-");
420452
const realDir = path.join(root, "real");
421453
const aliasDir = path.join(root, "alias");
422454
await fs.mkdir(realDir, { recursive: true });
423-
await fs.symlink(realDir, aliasDir);
455+
await fs.symlink(realDir, aliasDir, process.platform === "win32" ? "junction" : "dir");
424456

425457
await (await openRoot(root)).mkdir(path.join("alias", "nested", "deeper"));
426458

@@ -429,14 +461,14 @@ describe("fs-safe", () => {
429461
},
430462
);
431463

432-
it.runIf(process.platform !== "win32")(
464+
it.skipIf(!canCreateDirectorySymlinks)(
433465
"removes files through in-root symlink parents",
434466
async () => {
435467
const root = await tempDirs.make("openclaw-fs-safe-root-");
436468
const realDir = path.join(root, "real");
437469
const aliasDir = path.join(root, "alias");
438470
await fs.mkdir(realDir, { recursive: true });
439-
await fs.symlink(realDir, aliasDir);
471+
await fs.symlink(realDir, aliasDir, process.platform === "win32" ? "junction" : "dir");
440472
await fs.writeFile(path.join(realDir, "target.txt"), "hello");
441473

442474
await (await openRoot(root)).remove(path.join("alias", "target.txt"));
@@ -543,7 +575,7 @@ describe("fs-safe", () => {
543575
await expect(fs.readFile(outsideTarget, "utf8")).resolves.toBe("X".repeat(4096));
544576
});
545577

546-
it.runIf(process.platform !== "win32")(
578+
it.skipIf(!canCreateFileSymlinks)(
547579
"does not unlink out-of-root file when symlink retarget races remove",
548580
async () => {
549581
const { root, outside, slot, outsideTarget } = await setupSymlinkWriteRaceFixture({
@@ -567,7 +599,7 @@ describe("fs-safe", () => {
567599
},
568600
);
569601

570-
it.runIf(process.platform !== "win32")(
602+
it.skipIf(!canCreateFileSymlinks)(
571603
"does not create out-of-root directories when symlink retarget races mkdir",
572604
async () => {
573605
const root = await tempDirs.make("openclaw-fs-safe-root-");

0 commit comments

Comments
 (0)