ci+test: run check on windows and guard windows-only test behavior

Run the check job on windows-latest in addition to ubuntu so the
windows code paths (no O_NOFOLLOW, node fallbacks for fd-relative
ops, ACL inspection) are exercised on every PR rather than only
documented.

Make the test suite pass on the new windows runner by addressing
the platform-specific failures:

- Long happy-path tests that mix supported (mkdir, write, read) and
  unsupported (stat, list, move, exists) operations are guarded
  with skipIf(process.platform === "win32") since the pinned
  filesystem helper throws "unsupported-platform" on win32 by
  design (src/pinned-python.ts).
- Short focused tests where the unsupported operation is the whole
  point (pinned-python, pinned-write-fallback-coverage,
  write-boundary-bypass symlink-move) split into runIf(non-win32)
  and runIf(win32) tests, with the windows variant asserting
  unsupported-platform.
- The expectFsSafeCode helper accepts unsupported-platform on
  windows; new expectedFsSafeCode helper substitutes for
  per-rejects.toMatchObject sites where the windows code differs
  from posix (e.g. path-alias / not-found returning
  unsupported-platform via the helper layer).
- secure-file-reads test split into a posix happy-path runIf and a
  windows runIf that asserts permission-unverified, since ACL
  inspection has no portable equivalent on windows
  (src/secure-file.ts:177).
- safeFileURLToPath test uses hardcoded platform-specific input/
  output instead of building the URL via pathToFileURL+fileURLToPath
  so the assertion verifies the function directly.
- Fix expandHomePrefix to normalize path separators by splitting via
  path.normalize + path.sep and rejoining via path.join. Apply the
  same segment-based check to resolveHomeRelativePath and
  resolveOsHomeRelativePath. Drop input.trim() — whitespace is a
  valid filename character on both platforms and env-var inputs are
  already trimmed upstream via normalizeOptionalString.
- coverage-more's "normalizes empty temp names" decomposes the
  result with path.dirname/path.basename instead of regex-matching
  a path-separator literal.
- extracted-helpers' path-helpers test builds its root with
  path.resolve so the drive letter is present on windows.
- additional-boundary-bypass guards its "..\evil.txt" sanitizer
  assertion behind a non-win32 check (windows reserves "\" as a
  path separator and cannot have it in a filename).
- coverage-more's sibling temp test guards just the posix file-mode
  assertion (stat.mode & 0o777 === 0o600), which has no analog on
  windows. The syncing behaviour the test actually targets still
  runs on both platforms.
- Raise test/new-primitives.test.ts size budget to 1500 to
  accommodate the secure-file-reads test split.

After: 253 passed, 1 failed, 66 skipped on windows-11-arm64. The
single remaining failure is a separate library-side gap (a
SAFE_REJECTED_SUSPICIOUS_WRITE_PAYLOADS payload resolves on windows
instead of rejecting) and will be tracked in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sarah Fortune 2026-05-07 14:59:24 -07:00
parent c7ccb99d30
commit 3be7ba6ee3
15 changed files with 226 additions and 108 deletions

View File

@ -11,10 +11,10 @@ permissions:
contents: read
jobs:
check:
name: Node 22 check
lint-workflows:
name: Lint workflows
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 5
steps:
- name: Check out
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@ -22,6 +22,20 @@ jobs:
- name: Lint workflows
uses: rhysd/actionlint@914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12
check:
name: Node 22 check (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
steps:
- name: Check out
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up pnpm
uses: pnpm/action-setup@8912a9102ac27614460f54aedde9e1e7f9aec20d # v6.0.5
with:

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules/
coverage/
*.tsbuildinfo
.deepsec/
.vscode

View File

@ -9,7 +9,7 @@ const LINE_BUDGETS = new Map([
["src/root-impl.ts", 1744],
["src/root-path.ts", 862],
["test/api-coverage.test.ts", 982],
["test/new-primitives.test.ts", 998],
["test/new-primitives.test.ts", 1500],
]);
function walk(dir) {

View File

@ -87,7 +87,11 @@ export function expandHomePrefix(
homedir?: () => string;
},
): string {
if (!input.startsWith("~")) {
// Normalize and split into path segments. path.normalize converts "/"
// to the native separator on Windows and leaves "\" as a literal name
// character on POSIX, so the segment check is platform-correct.
const segments = path.normalize(input).split(path.sep);
if (segments[0] !== "~") {
return input;
}
const home =
@ -96,7 +100,7 @@ export function expandHomePrefix(
if (!home) {
return input;
}
return input.replace(/^~(?=$|[\\/])/, home);
return path.join(home, ...segments.slice(1));
}
export function resolveHomeRelativePath(
@ -106,19 +110,19 @@ export function resolveHomeRelativePath(
homedir?: () => string;
},
): string {
const trimmed = input.trim();
if (!trimmed) {
return trimmed;
if (!input) {
return input;
}
if (trimmed.startsWith("~")) {
const expanded = expandHomePrefix(trimmed, {
home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
env: opts?.env,
homedir: opts?.homedir,
});
return path.resolve(expanded);
const segments = path.normalize(input).split(path.sep)
if (segments[0] !== "~") {
return path.resolve(input);
}
return path.resolve(trimmed);
const expanded = expandHomePrefix(input, {
home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
env: opts?.env,
homedir: opts?.homedir,
});
return path.resolve(expanded);
}
export function resolveUserPath(
@ -145,17 +149,17 @@ export function resolveOsHomeRelativePath(
homedir?: () => string;
},
): string {
const trimmed = input.trim();
if (!trimmed) {
return trimmed;
if (!input) {
return input;
}
if (trimmed.startsWith("~")) {
const expanded = expandHomePrefix(trimmed, {
home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
env: opts?.env,
homedir: opts?.homedir,
});
return path.resolve(expanded);
const segments = path.normalize(input).split(path.sep);
if (segments[0] !== "~") {
return path.resolve(input);
}
return path.resolve(trimmed);
const expanded = expandHomePrefix(input, {
home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
env: opts?.env,
homedir: opts?.homedir,
});
return path.resolve(expanded);
}

View File

@ -73,7 +73,12 @@ describe("additional helper boundary bypass attempts", () => {
it("sanitizes temp file names and keeps temp file helpers inside their created directory", async () => {
const layout = await makeTempLayout("fs-safe-temp");
expect(sanitizeTempFileName("../../evil.txt")).toBe("evil.txt");
expect(sanitizeTempFileName("..\\evil.txt")).toBe("..-evil.txt");
if (process.platform !== "win32") {
// On windows "\" is a reserved path separator and cannot appear in a
// filename, so this case only exercises the posix sanitizer where "\"
// is a literal name character that needs neutralizing.
expect(sanitizeTempFileName("..\\evil.txt")).toBe("..-evil.txt");
}
expect(sanitizeTempFileName("\u0000../evil.txt")).toBe("evil.txt");
const target = await tempFile({ rootDir: layout.base, prefix: "../../prefix", fileName: "../../evil.txt" });

View File

@ -788,7 +788,7 @@ describe("temporary workspace and symlink parent helpers", () => {
});
describe("file stores and private stores", () => {
it("writes, streams, copies, reads, removes, and prunes file-store entries", async () => {
it.skipIf(process.platform === "win32")("writes, streams, copies, reads, removes, and prunes file-store entries", async () => {
const root = await tempRoot("fs-safe-store-");
const sourceRoot = await tempRoot("fs-safe-store-source-");
const source = path.join(sourceRoot, "source.txt");
@ -828,7 +828,7 @@ describe("file stores and private stores", () => {
await expect(fs.stat(old)).rejects.toMatchObject({ code: "ENOENT" });
});
it("covers private file store mode", async () => {
it.skipIf(process.platform === "win32")("covers private file store mode", async () => {
const root = await tempRoot("fs-safe-private-store-");
const store = fileStore({ rootDir: root, private: true });

View File

@ -121,9 +121,6 @@ describe("home directory helpers", () => {
expect(resolveHomeRelativePath("~/state", { env })).toBe(path.resolve("/configured/state"));
expect(resolveOsHomeRelativePath("~/state", { env })).toBe(path.resolve("/home/tester/state"));
expect(resolveUserPath("~/state", env)).toBe(path.resolve("/configured/state"));
expect(resolveUserPath(" ./relative ", { env })).toBe(path.resolve("./relative"));
expect(resolveHomeRelativePath(" ", { env })).toBe("");
expect(resolveOsHomeRelativePath(" ", { env })).toBe("");
});
it("ignores unusable home values", () => {

View File

@ -156,7 +156,10 @@ describe("sibling temp coverage", () => {
expect(result.filePath).toBe(path.join(root, "final.txt"));
await expect(fs.readFile(result.filePath, "utf8")).resolves.toBe("synced");
expect((await fs.stat(result.filePath)).mode & 0o777).toBe(0o600);
if (process.platform !== "win32") {
// POSIX file modes don't fully apply on Windows.
expect((await fs.stat(result.filePath)).mode & 0o777).toBe(0o600);
}
});
it("removes sibling temp files when copy-in rejects the staged source", async () => {
@ -188,15 +191,15 @@ describe("temp target edge coverage", () => {
const root = await tempRoot("fs-safe-temp-more-");
expect(sanitizeTempFileName("???")).toBe("download.bin");
expect(
buildRandomTempFilePath({
rootDir: root,
prefix: "!!!",
extension: "._-",
now: Number.NaN,
uuid: "id",
}),
).toMatch(new RegExp(`^${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/tmp-\\d+-id$`));
const built = buildRandomTempFilePath({
rootDir: root,
prefix: "!!!",
extension: "._-",
now: Number.NaN,
uuid: "id",
});
expect(path.dirname(built)).toBe(root);
expect(path.basename(built)).toMatch(/^tmp-\d+-id$/);
const tmp = await tempFile({ rootDir: root, prefix: "???", fileName: "???" });
expect(path.basename(tmp.dir)).toMatch(/^tmp-/);

View File

@ -1,5 +1,4 @@
import fs from "node:fs/promises";
import { fileURLToPath } from "node:url";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@ -43,8 +42,10 @@ afterEach(async () => {
describe("local file access helpers", () => {
it("accepts local file URLs and rejects remote hosts or encoded separators", () => {
const filePath = path.join(path.sep, "tmp", "demo.txt");
expect(safeFileURLToPath(new URL(`file://${filePath}`).href)).toBe(fileURLToPath(new URL(`file://${filePath}`)));
const [validUrl, expectedPath] = process.platform === "win32"
? ["file:///C:/tmp/demo.txt", "C:\\tmp\\demo.txt"]
: ["file:///tmp/demo.txt", "/tmp/demo.txt"];
expect(safeFileURLToPath(validUrl)).toBe(expectedPath);
expect(() => safeFileURLToPath("file://example.com/tmp/demo.txt")).toThrow(/remote hosts/);
expect(() => safeFileURLToPath("file:///tmp/a%2Fb.txt")).toThrow(/encode path separators/);
});
@ -57,10 +58,13 @@ describe("local file access helpers", () => {
describe("path helpers", () => {
it("checks containment and formats modes", () => {
const root = path.join(path.sep, "tmp", "root");
// Use path.resolve so on Windows the root carries a drive letter, which
// is what resolveSafeBaseDir / isPathInside both produce internally.
const root = path.resolve(path.sep, "tmp", "root");
const otherRoot = path.resolve(path.sep, "tmp", "root-other");
expect(resolveSafeBaseDir(root)).toBe(`${root}${path.sep}`);
expect(isWithinDir(root, path.join(root, "file.txt"))).toBe(true);
expect(isPathInside(root, path.join(path.sep, "tmp", "root-other"))).toBe(false);
expect(isPathInside(root, otherRoot)).toBe(false);
expect(formatPosixMode(0o100755)).toBe("755");
});
});

View File

@ -6,6 +6,9 @@ import { afterEach, describe, expect, it } from "vitest";
import { configureFsSafePython, FsSafeError, root as openRoot } from "../src/index.js";
import { openLocalFileSafely, readLocalFileSafely } from "../src/root.js";
import { __setFsSafeTestHooksForTest } from "../src/test-hooks.js";
import { expectedFsSafeCode } from "./helpers/security.js";
const skipOnWindows = process.platform === "win32";
const tempDirs: string[] = [];
@ -23,7 +26,7 @@ afterEach(async () => {
});
describe("@openclaw/fs-safe", () => {
it("reuses a root capability across filesystem operations", async () => {
it.skipIf(skipOnWindows)("reuses a root capability across filesystem operations", async () => {
const rootPath = await tempRoot("fs-root-object-");
const root = await openRoot(rootPath);
@ -62,7 +65,7 @@ describe("@openclaw/fs-safe", () => {
});
});
it("can disable the Python helper and keep root operations available", async () => {
it.skipIf(skipOnWindows)("can disable the Python helper and keep root operations available", async () => {
configureFsSafePython({ mode: "off" });
const rootPath = await tempRoot("fs-safe-python-off-");
const sourceRoot = await tempRoot("fs-safe-python-off-source-");
@ -125,7 +128,7 @@ describe("@openclaw/fs-safe", () => {
);
});
it("writes, reads, stats, and lists files within a root", async () => {
it.skipIf(skipOnWindows)("writes, reads, stats, and lists files within a root", async () => {
const root = await openRoot(await tempRoot("fs-safe-basic-"));
await root.mkdir("nested");
@ -234,7 +237,7 @@ describe("@openclaw/fs-safe", () => {
await expect(root.read("link/secret.txt")).rejects.toMatchObject({
code: "outside-workspace",
});
await expect(root.list("link")).rejects.toMatchObject({ code: "path-alias" });
await expect(root.list("link")).rejects.toMatchObject({ code: expectedFsSafeCode("path-alias") });
});
it("rejects symlink leaves for stat and read", async () => {
@ -244,11 +247,11 @@ describe("@openclaw/fs-safe", () => {
await writeFile(path.join(outside, "secret.txt"), "secret");
await symlink(path.join(outside, "secret.txt"), path.join(rootPath, "secret-link"), "file");
await expect(root.stat("secret-link")).rejects.toMatchObject({ code: "path-alias" });
await expect(root.stat("secret-link")).rejects.toMatchObject({ code: expectedFsSafeCode("path-alias") });
await expect(root.read("secret-link")).rejects.toMatchObject({ code: "symlink" });
});
it("renames paths within the same root and rejects symlink sources", async () => {
it.skipIf(skipOnWindows)("renames paths within the same root and rejects symlink sources", async () => {
const rootPath = await tempRoot("fs-safe-rename-");
const root = await openRoot(rootPath);
const outside = await tempRoot("fs-safe-outside-");
@ -264,7 +267,7 @@ describe("@openclaw/fs-safe", () => {
});
});
it("requires explicit overwrite for moves that replace a target", async () => {
it.skipIf(skipOnWindows)("requires explicit overwrite for moves that replace a target", async () => {
const rootPath = await tempRoot("fs-safe-rename-overwrite-");
const root = await openRoot(rootPath);
await root.write("from.txt", "source");
@ -279,7 +282,7 @@ describe("@openclaw/fs-safe", () => {
await expect(readFile(path.join(rootPath, "to.txt"), "utf8")).resolves.toBe("source");
});
it("enforces copyIn maxBytes while streaming", async () => {
it.skipIf(skipOnWindows)("enforces copyIn maxBytes while streaming", async () => {
const rootPath = await tempRoot("fs-safe-copy-limit-");
const sourceRoot = await tempRoot("fs-safe-copy-source-");
const sourcePath = path.join(sourceRoot, "source.txt");
@ -354,7 +357,7 @@ describe("@openclaw/fs-safe", () => {
await expect(readFile(outsideFile, "utf8")).resolves.toBe("kept");
await expect(root.stat("link")).rejects.toMatchObject({
code: "not-found",
code: expectedFsSafeCode("not-found"),
});
});

View File

@ -100,7 +100,13 @@ export async function makeTempLayout(
export function expectFsSafeCode(error: unknown, codes: readonly string[]): void {
expect(error).toBeInstanceOf(FsSafeError);
expect(codes).toContain((error as FsSafeError).code);
const accepted =
process.platform === "win32" ? [...codes, "unsupported-platform"] : codes;
expect(accepted).toContain((error as FsSafeError).code);
}
export function expectedFsSafeCode(code: string): string {
return process.platform === "win32" ? "unsupported-platform" : code;
}
export async function expectNoOutsideWrite(

View File

@ -125,7 +125,7 @@ describe("private temp workspaces", () => {
});
describe("file store", () => {
it("writes, reads, copies, and prunes files under the store root", async () => {
it.skipIf(process.platform === "win32")("writes, reads, copies, and prunes files under the store root", async () => {
const store = fileStore({ rootDir: root, maxBytes: 1024 });
await store.write("media/a.txt", "hello");
await expect(store.readBytes("media/a.txt")).resolves.toEqual(Buffer.from("hello"));
@ -175,7 +175,7 @@ describe("json store", () => {
});
describe("secure file reads", () => {
it("reads from a validated file handle", async () => {
it.runIf(process.platform !== "win32")("reads from a validated file handle", async () => {
const filePath = path.join(root, "secret.json");
await fs.writeFile(filePath, '{"token":"ok"}', { mode: 0o600 });
await fs.chmod(filePath, 0o600).catch(() => undefined);
@ -190,6 +190,24 @@ describe("secure file reads", () => {
expect(result.realPath).toBe(await fs.realpath(filePath));
});
it.runIf(process.platform === "win32")(
"fails closed on windows when ACL inspection is unavailable",
async () => {
// See src/secure-file.ts:177 — readSecureFile throws permission-unverified
// on Windows because ACL inspection has no portable equivalent.
const filePath = path.join(root, "secret.json");
await fs.writeFile(filePath, '{"token":"ok"}', { mode: 0o600 });
await expect(
readSecureFile({
filePath,
label: "test secret",
io: { maxBytes: 1024 },
}),
).rejects.toMatchObject({ code: "permission-unverified" });
},
);
it("rejects symlinks and files outside trusted dirs", async () => {
const trusted = path.join(root, "trusted");
const outside = path.join(root, "outside");

View File

@ -180,6 +180,18 @@ describe("Python helper configuration", () => {
describe("persistent Python helper worker", () => {
it("reuses one worker and unreferences it while idle", async () => {
if (process.platform === "win32") {
configureFsSafePython({ mode: "auto", pythonPath: "/tmp/fake-python" });
await expect(
runPinnedPythonOperation<{ ok: boolean }>({
operation: "stat",
rootPath: "/tmp/root",
payload: { relativePath: "a.txt" },
}),
).rejects.toMatchObject({ code: "unsupported-platform" });
return;
}
const child = makeRespondingChild();
spawnMock.mockReturnValue(child);
configureFsSafePython({ mode: "auto", pythonPath: "/tmp/fake-python" });
@ -209,6 +221,21 @@ describe("persistent Python helper worker", () => {
const rootDir = await tempRoot("fs-safe-python-policy-");
await fs.writeFile(path.join(rootDir, "file.txt"), "ok");
if (process.platform === "win32") {
spawnMock.mockImplementation(makeFailingChild);
configureFsSafePython({ mode: "auto", pythonPath: "/tmp/missing-python" });
const autoRoot = await root(rootDir);
await expect(autoRoot.stat("file.txt")).rejects.toMatchObject({
code: "unsupported-platform",
});
configureFsSafePython({ mode: "require", pythonPath: "/tmp/missing-python" });
await expect((await root(rootDir)).stat("file.txt")).rejects.toMatchObject({
code: "unsupported-platform",
});
return;
}
spawnMock.mockImplementation(makeFailingChild);
configureFsSafePython({ mode: "auto", pythonPath: "/tmp/missing-python" });
const autoRoot = await root(rootDir);

View File

@ -38,64 +38,89 @@ afterEach(async () => {
});
describe("pinned write fallback coverage", () => {
it("writes buffers, creates only when missing, streams, and enforces limits", async () => {
const { runPinnedWriteHelper } = await import("../src/pinned-write.js");
const root = await tempRoot("fs-safe-pinned-write-fallback-");
it.runIf(process.platform !== "win32")(
"writes buffers, creates only when missing, streams, and enforces limits",
async () => {
const { runPinnedWriteHelper } = await import("../src/pinned-write.js");
const root = await tempRoot("fs-safe-pinned-write-fallback-");
const created = await runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "created.txt",
mkdir: true,
mode: 0o600,
overwrite: false,
input: { kind: "buffer", data: "created", encoding: "utf8" },
});
expect(created.ino).toBeGreaterThan(0);
await expect(fs.readFile(path.join(root, "nested", "created.txt"), "utf8")).resolves.toBe(
"created",
);
await expect(
runPinnedWriteHelper({
const created = await runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "created.txt",
mkdir: true,
mode: 0o600,
overwrite: false,
input: { kind: "buffer", data: "again" },
}),
).rejects.toMatchObject({ code: "EEXIST" });
input: { kind: "buffer", data: "created", encoding: "utf8" },
});
expect(created.ino).toBeGreaterThan(0);
await expect(fs.readFile(path.join(root, "nested", "created.txt"), "utf8")).resolves.toBe(
"created",
);
await expect(
runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "created.txt",
mkdir: true,
mode: 0o600,
overwrite: false,
input: { kind: "buffer", data: "again" },
}),
).rejects.toMatchObject({ code: "EEXIST" });
const streamed = await runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "streamed.txt",
mkdir: true,
mode: 0o600,
overwrite: true,
maxBytes: 16,
input: { kind: "stream", stream: Readable.from(["stream", "ed"]) },
});
expect(streamed.dev).toBeGreaterThan(0);
await expect(fs.readFile(path.join(root, "nested", "streamed.txt"), "utf8")).resolves.toBe(
"streamed",
);
await expect(
runPinnedWriteHelper({
const streamed = await runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "too-large.txt",
basename: "streamed.txt",
mkdir: true,
mode: 0o600,
overwrite: true,
maxBytes: 2,
input: { kind: "buffer", data: Buffer.from("large") },
}),
).rejects.toMatchObject({ code: "too-large" });
await expect(fs.stat(path.join(root, "nested", "too-large.txt"))).rejects.toMatchObject({
code: "ENOENT",
});
});
maxBytes: 16,
input: { kind: "stream", stream: Readable.from(["stream", "ed"]) },
});
expect(streamed.dev).toBeGreaterThan(0);
await expect(fs.readFile(path.join(root, "nested", "streamed.txt"), "utf8")).resolves.toBe(
"streamed",
);
await expect(
runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "too-large.txt",
mkdir: true,
mode: 0o600,
overwrite: true,
maxBytes: 2,
input: { kind: "buffer", data: Buffer.from("large") },
}),
).rejects.toMatchObject({ code: "too-large" });
await expect(fs.stat(path.join(root, "nested", "too-large.txt"))).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it.runIf(process.platform === "win32")(
"rejects with unsupported-platform on windows",
async () => {
// fd-relative pinned filesystem operations are unavailable on windows
// (see src/pinned-python.ts), so the helper fails closed before any
// posix-only logic runs.
const { runPinnedWriteHelper } = await import("../src/pinned-write.js");
const root = await tempRoot("fs-safe-pinned-write-fallback-");
await expect(
runPinnedWriteHelper({
rootPath: root,
relativeParentPath: "nested",
basename: "created.txt",
mkdir: true,
mode: 0o600,
overwrite: false,
input: { kind: "buffer", data: "created", encoding: "utf8" },
}),
).rejects.toMatchObject({ code: "unsupported-platform" });
},
);
});

View File

@ -168,6 +168,17 @@ describe("write, move, and delete boundary bypass attempts", () => {
await fsp.symlink(layout.outsideFile, path.join(layout.root, "dest-link.txt"), "file");
const safeRoot = await openRoot(layout.root);
if (process.platform === "win32") {
await expect(safeRoot.move("source-link.txt", "moved.txt")).rejects.toMatchObject({
code: "unsupported-platform",
});
await expect(safeRoot.move("from.txt", "dest-link.txt", { overwrite: true })).rejects.toMatchObject({
code: "unsupported-platform",
});
await expectNoOutsideWrite(layout);
return;
}
await expect(safeRoot.move("source-link.txt", "moved.txt")).rejects.toBeTruthy();
await safeRoot.move("from.txt", "dest-link.txt", { overwrite: true });
await expectNoOutsideWrite(layout);