import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs/promises"; import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; import { WorkspaceService } from "../src/workspace-service.js"; async function makeWorkspace() { const root = await fs.mkdtemp(path.join(os.tmpdir(), "workspace-mcp-")); return { root, cleanup: async () => { await fs.rm(root, { recursive: true, force: true }); } }; } test("write_file and read_file operate within the workspace root", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const writeResult = await service.writeFile({ path: "src/example.txt", content: "hello workspace" }); assert.equal(writeResult.created, true); const readResult = await service.readFile({ path: "src/example.txt" }); assert.equal(readResult.content, "hello workspace"); assert.equal(readResult.path, "src/example.txt"); } finally { await workspace.cleanup(); } }); test("session context does not add a directory to workspace paths", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const context = { sessionId: "sess_123" }; await service.writeFile( { path: "note.txt", content: "session-bound" }, context ); const stored = await fs.readFile(path.join(workspace.root, "note.txt"), "utf8"); assert.equal(stored, "session-bound"); const readResult = await service.readFile({ path: "note.txt" }, context); assert.equal(readResult.content, "session-bound"); } finally { await workspace.cleanup(); } }); test("workspace context supports an additional subpath independent of session ID", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const context = { sessionId: "sess_789", workspaceSubpath: "feature-a/sources" }; await service.writeFile( { path: "note.txt", content: "nested-session-bound" }, context ); const stored = await fs.readFile( path.join(workspace.root, "feature-a", "sources", "note.txt"), "utf8" ); assert.equal(stored, "nested-session-bound"); } finally { await workspace.cleanup(); } }); test("ensureWorkspace creates the execution subpath before the first tool call", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const result = service.ensureWorkspace({ sessionId: "sess_789", workspaceSubpath: "execution-123" }); assert.equal(result.workspaceSubpath, "execution-123"); assert.equal(fsSync.statSync(path.join(workspace.root, "execution-123")).isDirectory(), true); } finally { await workspace.cleanup(); } }); test("workspace context rejects traversal in workspaceSubpath", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.writeFile( { path: "note.txt", content: "x" }, { sessionId: "sess_bad", workspaceSubpath: "../escape" } ), /workspaceSubpath/ ); } finally { await workspace.cleanup(); } }); test("read_binary_file and write_binary_file round-trip base64 content", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const original = Buffer.from([0, 1, 2, 255, 42]); const writeResult = await service.writeBinaryFile({ path: "assets/blob.bin", contentBase64: original.toString("base64") }); assert.equal(writeResult.created, true); const readResult = await service.readBinaryFile({ path: "assets/blob.bin" }); assert.equal(readResult.encoding, "base64"); assert.deepEqual(Buffer.from(readResult.contentBase64, "base64"), original); } finally { await workspace.cleanup(); } }); test("list_files returns recursive directory entries", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "a", "b"), { recursive: true }); await fs.writeFile(path.join(workspace.root, "a", "b", "file.txt"), "data", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const result = await service.listFiles({ path: "a", recursive: true }); assert.deepEqual( result.entries.map((entry) => entry.path), ["a/b", "a/b/file.txt"] ); } finally { await workspace.cleanup(); } }); test("list_files reports an absent path without failing the tool call", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const result = await service.listFiles({ path: ".orchestrator" }); assert.deepEqual(result, { root: workspace.root, path: ".orchestrator", exists: false, entries: [], truncated: false }); } finally { await workspace.cleanup(); } }); test("an empty workspace path is normalized to the workspace root", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "root.txt"), "root", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const listed = await service.listFiles({ path: "" }); const info = await service.fileInfo({ path: "" }); assert.equal(listed.path, "."); assert.equal(listed.entries[0].path, "root.txt"); assert.equal(info.path, "."); assert.equal(info.type, "directory"); } finally { await workspace.cleanup(); } }); test("search_text finds matches with line and column information", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "src"), { recursive: true }); await fs.writeFile(path.join(workspace.root, "src", "one.txt"), "alpha\nbeta alpha\n", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const result = await service.searchText({ query: "alpha" }); assert.equal(result.matches.length, 2); assert.deepEqual(result.matches[0], { path: "src/one.txt", line: 1, column: 1, preview: "alpha" }); assert.deepEqual(result.matches[1], { path: "src/one.txt", line: 2, column: 6, preview: "beta alpha" }); } finally { await workspace.cleanup(); } }); test("apply_patch updates an existing file", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "notes.txt"), "one\ntwo\nthree\n", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const patch = [ "*** Begin Patch", "*** Update File: notes.txt", "@@", " one", "-two", "+TWO", " three", "*** End Patch" ].join("\n"); const result = await service.applyPatch({ path: "notes.txt", patch }); assert.equal(result.action, "update"); assert.equal(result.hunksApplied, 1); const updated = await fs.readFile(path.join(workspace.root, "notes.txt"), "utf8"); assert.equal(updated, "one\nTWO\nthree\n"); } finally { await workspace.cleanup(); } }); test("apply_patch accepts a standard unified diff", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "notes.txt"), "one\ntwo\nthree\n", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const patch = [ "diff --git a/notes.txt b/notes.txt", "index 4cb29ea..c7b5ac4 100644", "--- a/notes.txt", "+++ b/notes.txt", "@@ -1,3 +1,4 @@", " one", "-two", "+TWO", "+inserted", " three" ].join("\n"); const result = await service.applyPatch({ path: "notes.txt", patch }); assert.equal(result.action, "update"); assert.equal(result.hunksApplied, 1); assert.equal(await fs.readFile(path.join(workspace.root, "notes.txt"), "utf8"), "one\nTWO\ninserted\nthree\n"); } finally { await workspace.cleanup(); } }); test("a unified diff can insert a line without surrounding context", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "notes.txt"), "one\ntwo\n", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const patch = [ "--- a/notes.txt", "+++ b/notes.txt", "@@ -1,0 +2 @@", "+inserted" ].join("\n"); await service.applyPatch({ path: "notes.txt", patch }); assert.equal(await fs.readFile(path.join(workspace.root, "notes.txt"), "utf8"), "one\ninserted\ntwo\n"); } finally { await workspace.cleanup(); } }); test("apply_patch can add a new file", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "src"), { recursive: true }); const service = new WorkspaceService({ roots: [workspace.root] }); const patch = [ "*** Begin Patch", "*** Add File: src/new.txt", "@@", "+hello", "+world", "*** End Patch" ].join("\n"); const result = await service.applyPatch({ path: "src/new.txt", patch }); assert.equal(result.action, "add"); const created = await fs.readFile(path.join(workspace.root, "src", "new.txt"), "utf8"); assert.equal(created, "hello\nworld\n"); } finally { await workspace.cleanup(); } }); test("apply_patch can delete an existing file", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "obsolete.txt"), "gone\nsoon\n", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const patch = [ "*** Begin Patch", "*** Delete File: obsolete.txt", "@@", "-gone", "-soon", "*** End Patch" ].join("\n"); const result = await service.applyPatch({ path: "obsolete.txt", patch }); assert.equal(result.action, "delete"); assert.equal(fsSync.existsSync(path.join(workspace.root, "obsolete.txt")), false); } finally { await workspace.cleanup(); } }); test("file_info returns metadata without reading file content", async () => { const workspace = await makeWorkspace(); try { await fs.writeFile(path.join(workspace.root, "meta.txt"), "content", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const info = await service.fileInfo({ path: "meta.txt" }); assert.equal(info.exists, true); assert.equal(info.type, "file"); assert.equal(info.path, "meta.txt"); assert.equal(typeof info.size, "number"); } finally { await workspace.cleanup(); } }); test("file_info reports an absent path without failing the tool call", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const info = await service.fileInfo({ path: ".orchestrator" }); assert.deepEqual(info, { root: workspace.root, path: ".orchestrator", exists: false }); } finally { await workspace.cleanup(); } }); test("delete_path removes directories only when recursive is enabled", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "tree", "child"), { recursive: true }); await fs.writeFile(path.join(workspace.root, "tree", "child", "file.txt"), "x", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.deletePath({ path: "tree" }), /recursive=true/ ); const result = await service.deletePath({ path: "tree", recursive: true }); assert.equal(result.deleted, true); assert.equal(fsSync.existsSync(path.join(workspace.root, "tree")), false); } finally { await workspace.cleanup(); } }); test("move_path relocates files and rename_path renames them", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "src"), { recursive: true }); await fs.writeFile(path.join(workspace.root, "src", "before.txt"), "payload", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const moved = await service.movePath({ fromPath: "src/before.txt", toPath: "dist/after.txt" }); assert.equal(moved.moved, true); assert.equal(fsSync.existsSync(path.join(workspace.root, "src", "before.txt")), false); assert.equal(fsSync.existsSync(path.join(workspace.root, "dist", "after.txt")), true); const renamed = await service.renamePath({ path: "dist/after.txt", newName: "final.txt" }); assert.equal(renamed.moved, true); assert.equal(fsSync.existsSync(path.join(workspace.root, "dist", "after.txt")), false); assert.equal(fsSync.existsSync(path.join(workspace.root, "dist", "final.txt")), true); } finally { await workspace.cleanup(); } }); test("move_path can overwrite an existing destination when requested", async () => { const workspace = await makeWorkspace(); try { await fs.mkdir(path.join(workspace.root, "a"), { recursive: true }); await fs.mkdir(path.join(workspace.root, "b"), { recursive: true }); await fs.writeFile(path.join(workspace.root, "a", "from.txt"), "new", "utf8"); await fs.writeFile(path.join(workspace.root, "b", "to.txt"), "old", "utf8"); const service = new WorkspaceService({ roots: [workspace.root] }); const result = await service.movePath({ fromPath: "a/from.txt", toPath: "b/to.txt", overwrite: true }); assert.equal(result.overwritten, true); const content = await fs.readFile(path.join(workspace.root, "b", "to.txt"), "utf8"); assert.equal(content, "new"); } finally { await workspace.cleanup(); } }); test("path traversal outside the root is rejected", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.readFile({ path: "../etc/passwd" }), /escapes allowed workspace root/ ); } finally { await workspace.cleanup(); } }); test("absolute paths and caller-selected roots are rejected", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.writeFile({ path: path.join(workspace.root, "outside.txt"), content: "x" }), /relative path/ ); await assert.rejects( service.writeFile({ path: "inside.txt", root: workspace.root, content: "x" }), /Selecting a workspace root/ ); } finally { await workspace.cleanup(); } }); test("read_file rejects direct symlink targets", async () => { const workspace = await makeWorkspace(); try { const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "workspace-mcp-outside-")); const outsideFile = path.join(outsideDir, "secret.txt"); await fs.writeFile(outsideFile, "secret", "utf8"); await fs.symlink(outsideFile, path.join(workspace.root, "link.txt")); const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.readFile({ path: "link.txt" }), /Symlink targets are not allowed/ ); await fs.rm(outsideDir, { recursive: true, force: true }); } finally { await workspace.cleanup(); } }); test("read_file rejects symlinked parent segments", async () => { const workspace = await makeWorkspace(); try { const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "workspace-mcp-outside-parent-")); await fs.writeFile(path.join(outsideDir, "secret.txt"), "secret", "utf8"); await fs.symlink(outsideDir, path.join(workspace.root, "linked-dir")); const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.readFile({ path: "linked-dir/secret.txt" }), /Symlink path segments are not allowed/ ); await fs.rm(outsideDir, { recursive: true, force: true }); } finally { await workspace.cleanup(); } }); test("make_directory creates directories safely", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); const result = await service.makeDirectory({ path: "nested/path" }); assert.equal(result.created, true); assert.equal(fsSync.existsSync(path.join(workspace.root, "nested", "path")), true); } finally { await workspace.cleanup(); } }); test("delete_path refuses to delete the workspace root", async () => { const workspace = await makeWorkspace(); try { const service = new WorkspaceService({ roots: [workspace.root] }); await assert.rejects( service.deletePath({ path: "." }), /workspace root/ ); } finally { await workspace.cleanup(); } });