Archived
test: cover workspace file services
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { readWorkspaceFile } from "./fileContentService.js";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
|
||||||
|
async function tempWorkspace(): Promise<string> {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "pi-web-file-content-"));
|
||||||
|
roots.push(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readWorkspaceFile", () => {
|
||||||
|
it("reads text files with normalized paths and language metadata", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await mkdir(join(root, "src"));
|
||||||
|
await writeFile(join(root, "src", "main.ts"), "const answer = 42;\n");
|
||||||
|
|
||||||
|
const file = await readWorkspaceFile(root, "./src//main.ts");
|
||||||
|
|
||||||
|
expect(file).toMatchObject({
|
||||||
|
path: "src/main.ts",
|
||||||
|
language: "typescript",
|
||||||
|
encoding: "utf8",
|
||||||
|
content: "const answer = 42;\n",
|
||||||
|
truncated: false,
|
||||||
|
binary: false,
|
||||||
|
});
|
||||||
|
expect(file.size).toBe(19);
|
||||||
|
expect(Date.parse(file.modifiedAt)).not.toBeNaN();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects missing paths, directories, traversal, and absolute paths", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await mkdir(join(root, "dir"));
|
||||||
|
|
||||||
|
await expect(readWorkspaceFile(root, undefined)).rejects.toThrow("path query parameter is required");
|
||||||
|
await expect(readWorkspaceFile(root, "dir")).rejects.toThrow("Path is not a file");
|
||||||
|
await expect(readWorkspaceFile(root, "../secret.txt")).rejects.toThrow("Path traversal is not allowed");
|
||||||
|
await expect(readWorkspaceFile(root, "/etc/passwd")).rejects.toThrow("Absolute paths are not allowed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects binary files and omits binary content", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await writeFile(join(root, "image.bin"), Buffer.from([0x66, 0x6f, 0x00, 0x6f]));
|
||||||
|
|
||||||
|
const file = await readWorkspaceFile(root, "image.bin");
|
||||||
|
|
||||||
|
expect(file).toMatchObject({ content: "", binary: true, truncated: false });
|
||||||
|
expect(file.size).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates large text files", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await writeFile(join(root, "large.md"), "a".repeat(512 * 1024 + 7));
|
||||||
|
|
||||||
|
const file = await readWorkspaceFile(root, "large.md");
|
||||||
|
|
||||||
|
expect(file.language).toBe("markdown");
|
||||||
|
expect(file.content).toHaveLength(512 * 1024);
|
||||||
|
expect(file.truncated).toBe(true);
|
||||||
|
expect(file.binary).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import { listWorkspaceTree } from "./fileTreeService.js";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
|
||||||
|
async function tempWorkspace(): Promise<string> {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "pi-web-file-tree-"));
|
||||||
|
roots.push(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("listWorkspaceTree", () => {
|
||||||
|
it("lists visible entries with directories first, sorted by name", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await mkdir(join(root, "z-dir"));
|
||||||
|
await mkdir(join(root, "a-dir"));
|
||||||
|
await mkdir(join(root, ".git"));
|
||||||
|
await mkdir(join(root, "node_modules"));
|
||||||
|
await writeFile(join(root, "b.txt"), "b");
|
||||||
|
await writeFile(join(root, "a.txt"), "a");
|
||||||
|
await symlink(join(root, "a.txt"), join(root, "link.txt"));
|
||||||
|
|
||||||
|
const tree = await listWorkspaceTree(root, undefined);
|
||||||
|
|
||||||
|
expect(tree.path).toBe("");
|
||||||
|
expect(tree.truncated).toBe(false);
|
||||||
|
expect(tree.entries.map((entry) => [entry.name, entry.type])).toEqual([
|
||||||
|
["a-dir", "directory"],
|
||||||
|
["z-dir", "directory"],
|
||||||
|
["a.txt", "file"],
|
||||||
|
["b.txt", "file"],
|
||||||
|
["link.txt", "symlink"],
|
||||||
|
]);
|
||||||
|
expect(Date.parse(tree.scannedAt)).not.toBeNaN();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists nested directories using normalized relative paths", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await mkdir(join(root, "src", "client"), { recursive: true });
|
||||||
|
await writeFile(join(root, "src", "client", "main.ts"), "");
|
||||||
|
|
||||||
|
const tree = await listWorkspaceTree(root, "./src//client");
|
||||||
|
|
||||||
|
expect(tree.path).toBe("src/client");
|
||||||
|
expect(tree.entries).toHaveLength(1);
|
||||||
|
expect(tree.entries[0]).toMatchObject({ name: "main.ts", path: "src/client/main.ts", type: "file" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-directory targets and unsafe paths", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await writeFile(join(root, "file.txt"), "content");
|
||||||
|
|
||||||
|
await expect(listWorkspaceTree(root, "file.txt")).rejects.toThrow("Path is not a directory");
|
||||||
|
await expect(listWorkspaceTree(root, "../outside")).rejects.toThrow("Path traversal is not allowed");
|
||||||
|
await expect(listWorkspaceTree(root, "/tmp")).rejects.toThrow("Absolute paths are not allowed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks responses as truncated after the service entry limit", async () => {
|
||||||
|
const root = await tempWorkspace();
|
||||||
|
await Promise.all(Array.from({ length: 1001 }, (_, index) => writeFile(join(root, `${String(index).padStart(4, "0")}.txt`), "")));
|
||||||
|
|
||||||
|
const tree = await listWorkspaceTree(root, undefined);
|
||||||
|
|
||||||
|
expect(tree.entries).toHaveLength(1000);
|
||||||
|
expect(tree.truncated).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user