test: add outside-workspace error mapping coverage

This commit is contained in:
Ayaan Zaidi
2026-02-28 17:55:21 +05:30
committed by Ayaan Zaidi
parent d6552998e9
commit 44220ef24a
4 changed files with 193 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
type CapturedEditOperations = {
access: (absolutePath: string) => Promise<void>;
};
const mocks = vi.hoisted(() => ({
operations: undefined as CapturedEditOperations | undefined,
}));
vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
const actual = await importOriginal<typeof import("@mariozechner/pi-coding-agent")>();
return {
...actual,
createEditTool: (_cwd: string, options?: { operations?: CapturedEditOperations }) => {
mocks.operations = options?.operations;
return {
name: "edit",
description: "test edit tool",
parameters: { type: "object", properties: {} },
execute: async () => ({
content: [{ type: "text" as const, text: "ok" }],
}),
};
},
};
});
const { createHostWorkspaceEditTool } = await import("./pi-tools.read.js");
describe("createHostWorkspaceEditTool host access mapping", () => {
let tmpDir = "";
afterEach(async () => {
mocks.operations = undefined;
if (tmpDir) {
await fs.rm(tmpDir, { recursive: true, force: true });
tmpDir = "";
}
});
it.runIf(process.platform !== "win32")(
"maps outside-workspace safe-open failures to EACCES",
async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-edit-access-test-"));
const workspaceDir = path.join(tmpDir, "workspace");
const outsideDir = path.join(tmpDir, "outside");
const linkDir = path.join(workspaceDir, "escape");
const outsideFile = path.join(outsideDir, "secret.txt");
await fs.mkdir(workspaceDir, { recursive: true });
await fs.mkdir(outsideDir, { recursive: true });
await fs.writeFile(outsideFile, "secret", "utf8");
await fs.symlink(outsideDir, linkDir);
createHostWorkspaceEditTool(workspaceDir, { workspaceOnly: true });
expect(mocks.operations).toBeDefined();
await expect(
mocks.operations!.access(path.join(workspaceDir, "escape", "secret.txt")),
).rejects.toMatchObject({ code: "EACCES" });
},
);
});

View File

@@ -141,6 +141,28 @@ describe("resolveExistingPathsWithinRoot", () => {
},
);
it.runIf(process.platform !== "win32")(
"returns outside-root message for files reached via escaping symlinked directories",
async () => {
await withFixtureRoot(async ({ baseDir, uploadsDir }) => {
const outsideDir = path.join(baseDir, "outside");
await fs.mkdir(outsideDir, { recursive: true });
await fs.writeFile(path.join(outsideDir, "secret.txt"), "secret", "utf8");
await fs.symlink(outsideDir, path.join(uploadsDir, "alias"));
const result = await resolveWithinUploads({
uploadsDir,
requestedPaths: ["alias/secret.txt"],
});
expect(result).toEqual({
ok: false,
error: "File is outside uploads directory",
});
});
},
);
it.runIf(process.platform !== "win32")(
"accepts canonical absolute paths when upload root is a symlink alias",
async () => {

View File

@@ -0,0 +1,59 @@
import fs from "node:fs/promises";
import type { AddressInfo } from "node:net";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
openFileWithinRoot: vi.fn(),
cleanOldMedia: vi.fn().mockResolvedValue(undefined),
}));
let mediaDir = "";
vi.mock("../infra/fs-safe.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/fs-safe.js")>();
return {
...actual,
openFileWithinRoot: mocks.openFileWithinRoot,
};
});
vi.mock("./store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./store.js")>();
return {
...actual,
getMediaDir: () => mediaDir,
cleanOldMedia: mocks.cleanOldMedia,
};
});
const { SafeOpenError } = await import("../infra/fs-safe.js");
const { startMediaServer } = await import("./server.js");
describe("media server outside-workspace mapping", () => {
let server: Awaited<ReturnType<typeof startMediaServer>>;
let port = 0;
beforeAll(async () => {
mediaDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-outside-workspace-"));
server = await startMediaServer(0, 1_000);
port = (server.address() as AddressInfo).port;
});
afterAll(async () => {
await new Promise((resolve) => server.close(resolve));
await fs.rm(mediaDir, { recursive: true, force: true });
mediaDir = "";
});
it("returns 400 with a specific outside-workspace message", async () => {
mocks.openFileWithinRoot.mockRejectedValueOnce(
new SafeOpenError("outside-workspace", "file is outside workspace root"),
);
const response = await fetch(`http://127.0.0.1:${port}/media/ok-id`);
expect(response.status).toBe(400);
expect(await response.text()).toBe("file is outside workspace root");
});
});

View File

@@ -0,0 +1,46 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
const mocks = vi.hoisted(() => ({
readLocalFileSafely: vi.fn(),
}));
vi.mock("../infra/fs-safe.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../infra/fs-safe.js")>();
return {
...actual,
readLocalFileSafely: mocks.readLocalFileSafely,
};
});
const { saveMediaSource } = await import("./store.js");
const { SafeOpenError } = await import("../infra/fs-safe.js");
describe("media store outside-workspace mapping", () => {
let tempHome: TempHomeEnv;
let home = "";
beforeAll(async () => {
tempHome = await createTempHomeEnv("openclaw-media-store-test-home-");
home = tempHome.home;
});
afterAll(async () => {
await tempHome.restore();
});
it("maps outside-workspace reads to a descriptive invalid-path error", async () => {
const sourcePath = path.join(home, "outside-media.txt");
await fs.writeFile(sourcePath, "hello");
mocks.readLocalFileSafely.mockRejectedValueOnce(
new SafeOpenError("outside-workspace", "file is outside workspace root"),
);
await expect(saveMediaSource(sourcePath)).rejects.toMatchObject({
code: "invalid-path",
message: "Media path is outside workspace root",
});
});
});