diff options
Diffstat (limited to 'packages/tool-read-file/src/read-file.test.ts')
| -rw-r--r-- | packages/tool-read-file/src/read-file.test.ts | 688 |
1 files changed, 344 insertions, 344 deletions
diff --git a/packages/tool-read-file/src/read-file.test.ts b/packages/tool-read-file/src/read-file.test.ts index bac5902..785769a 100644 --- a/packages/tool-read-file/src/read-file.test.ts +++ b/packages/tool-read-file/src/read-file.test.ts @@ -5,25 +5,25 @@ import { localExecBackend } from "@dispatch/exec-backend"; import { createLogger, type ToolExecuteContext } from "@dispatch/kernel"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { - createReadFileTool, - formatDirectoryEntries, - renderLines, - sliceLines, - validateArgs, + createReadFileTool, + formatDirectoryEntries, + renderLines, + sliceLines, + validateArgs, } from "./read-file.js"; function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext { - return { - toolCallId: "test-call-1", - onOutput: () => {}, - signal: AbortSignal.timeout(5000), - log: createLogger( - { extensionId: "test" }, - { emit: () => {} }, - { now: () => 0, newId: () => "id" }, - ), - ...overrides, - }; + return { + toolCallId: "test-call-1", + onOutput: () => {}, + signal: AbortSignal.timeout(5000), + log: createLogger( + { extensionId: "test" }, + { emit: () => {} }, + { now: () => 0, newId: () => "id" }, + ), + ...overrides, + }; } /** @@ -32,355 +32,355 @@ function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext { * real fs edge is exercised, matching the constitution's strict-core rule. */ function makeTool(workdir: string) { - return createReadFileTool({ resolveBackend: () => localExecBackend, workdir }); + return createReadFileTool({ resolveBackend: () => localExecBackend, workdir }); } let workdir: string; beforeEach(async () => { - workdir = await mkdtemp(join(tmpdir(), "tool-read-file-test-")); + workdir = await mkdtemp(join(tmpdir(), "tool-read-file-test-")); }); afterEach(async () => { - await rm(workdir, { recursive: true, force: true }); + await rm(workdir, { recursive: true, force: true }); }); describe("validateArgs", () => { - it("returns validated args for valid input", () => { - const result = validateArgs({ path: "foo.txt" }); - expect(result).toEqual({ path: "foo.txt", offset: 1, limit: 500 }); - }); - - it("parses offset and limit", () => { - const result = validateArgs({ path: "foo.txt", offset: 5, limit: 10 }); - expect(result).toEqual({ path: "foo.txt", offset: 5, limit: 10 }); - }); - - it("clamps limit to hard cap of 5000", () => { - const result = validateArgs({ path: "foo.txt", limit: 99999 }); - expect(result).toEqual({ path: "foo.txt", offset: 1, limit: 5000 }); - }); - - it("returns error for null args", () => { - const result = validateArgs(null); - expect(result).toHaveProperty("error"); - }); - - it("returns error for missing path", () => { - const result = validateArgs({}); - expect(result).toHaveProperty("error"); - }); - - it("returns error for non-string path", () => { - const result = validateArgs({ path: 123 }); - expect(result).toHaveProperty("error"); - }); - - it("returns error for invalid offset", () => { - const result = validateArgs({ path: "foo.txt", offset: -1 }); - expect(result).toHaveProperty("error"); - }); - - it("returns error for invalid limit", () => { - const result = validateArgs({ path: "foo.txt", limit: 0 }); - expect(result).toHaveProperty("error"); - }); + it("returns validated args for valid input", () => { + const result = validateArgs({ path: "foo.txt" }); + expect(result).toEqual({ path: "foo.txt", offset: 1, limit: 500 }); + }); + + it("parses offset and limit", () => { + const result = validateArgs({ path: "foo.txt", offset: 5, limit: 10 }); + expect(result).toEqual({ path: "foo.txt", offset: 5, limit: 10 }); + }); + + it("clamps limit to hard cap of 5000", () => { + const result = validateArgs({ path: "foo.txt", limit: 99999 }); + expect(result).toEqual({ path: "foo.txt", offset: 1, limit: 5000 }); + }); + + it("returns error for null args", () => { + const result = validateArgs(null); + expect(result).toHaveProperty("error"); + }); + + it("returns error for missing path", () => { + const result = validateArgs({}); + expect(result).toHaveProperty("error"); + }); + + it("returns error for non-string path", () => { + const result = validateArgs({ path: 123 }); + expect(result).toHaveProperty("error"); + }); + + it("returns error for invalid offset", () => { + const result = validateArgs({ path: "foo.txt", offset: -1 }); + expect(result).toHaveProperty("error"); + }); + + it("returns error for invalid limit", () => { + const result = validateArgs({ path: "foo.txt", limit: 0 }); + expect(result).toHaveProperty("error"); + }); }); describe("sliceLines", () => { - it("returns all lines with offset=1, limit=500", () => { - const content = "line1\nline2\nline3"; - const result = sliceLines(content, 1, 500); - expect(result.lines).toEqual(["line1", "line2", "line3"]); - expect(result.totalLines).toBe(3); - }); - - it("slices with offset", () => { - const content = "line1\nline2\nline3\nline4"; - const result = sliceLines(content, 2, 2); - expect(result.lines).toEqual(["line2", "line3"]); - expect(result.totalLines).toBe(4); - }); - - it("handles offset beyond content", () => { - const content = "line1\nline2"; - const result = sliceLines(content, 10, 5); - expect(result.lines).toEqual([]); - expect(result.totalLines).toBe(2); - }); - - it("handles single line (no newline)", () => { - const content = "only line"; - const result = sliceLines(content, 1, 10); - expect(result.lines).toEqual(["only line"]); - expect(result.totalLines).toBe(1); - }); + it("returns all lines with offset=1, limit=500", () => { + const content = "line1\nline2\nline3"; + const result = sliceLines(content, 1, 500); + expect(result.lines).toEqual(["line1", "line2", "line3"]); + expect(result.totalLines).toBe(3); + }); + + it("slices with offset", () => { + const content = "line1\nline2\nline3\nline4"; + const result = sliceLines(content, 2, 2); + expect(result.lines).toEqual(["line2", "line3"]); + expect(result.totalLines).toBe(4); + }); + + it("handles offset beyond content", () => { + const content = "line1\nline2"; + const result = sliceLines(content, 10, 5); + expect(result.lines).toEqual([]); + expect(result.totalLines).toBe(2); + }); + + it("handles single line (no newline)", () => { + const content = "only line"; + const result = sliceLines(content, 1, 10); + expect(result.lines).toEqual(["only line"]); + expect(result.totalLines).toBe(1); + }); }); describe("renderLines", () => { - it("renders lines with 1-indexed line numbers", () => { - const result = renderLines(["a", "b", "c"], 1); - expect(result).toBe("1: a\n2: b\n3: c"); - }); - - it("renders with custom offset", () => { - const result = renderLines(["x", "y"], 10); - expect(result).toBe("10: x\n11: y"); - }); + it("renders lines with 1-indexed line numbers", () => { + const result = renderLines(["a", "b", "c"], 1); + expect(result).toBe("1: a\n2: b\n3: c"); + }); + + it("renders with custom offset", () => { + const result = renderLines(["x", "y"], 10); + expect(result).toBe("10: x\n11: y"); + }); }); describe("formatDirectoryEntries", () => { - it("lists directory entries sorted with trailing slash on subdirectories", () => { - const entries = [ - { name: "zebra.txt", isDirectory: false }, - { name: "alpha", isDirectory: true }, - { name: "readme.md", isDirectory: false }, - { name: "beta", isDirectory: true }, - ]; - const result = formatDirectoryEntries(entries, "mydir"); - expect(result).toBe("alpha/\nbeta/\nreadme.md\nzebra.txt"); - }); - - it("returns empty-directory message for an empty dir", () => { - const result = formatDirectoryEntries([], "empty-dir"); - expect(result).toBe("(empty directory: empty-dir)"); - }); - - it("handles mixed files and directories with same name sorting", () => { - const entries = [ - { name: "b", isDirectory: false }, - { name: "a", isDirectory: true }, - ]; - const result = formatDirectoryEntries(entries, "."); - expect(result).toBe("a/\nb"); - }); + it("lists directory entries sorted with trailing slash on subdirectories", () => { + const entries = [ + { name: "zebra.txt", isDirectory: false }, + { name: "alpha", isDirectory: true }, + { name: "readme.md", isDirectory: false }, + { name: "beta", isDirectory: true }, + ]; + const result = formatDirectoryEntries(entries, "mydir"); + expect(result).toBe("alpha/\nbeta/\nreadme.md\nzebra.txt"); + }); + + it("returns empty-directory message for an empty dir", () => { + const result = formatDirectoryEntries([], "empty-dir"); + expect(result).toBe("(empty directory: empty-dir)"); + }); + + it("handles mixed files and directories with same name sorting", () => { + const entries = [ + { name: "b", isDirectory: false }, + { name: "a", isDirectory: true }, + ]; + const result = formatDirectoryEntries(entries, "."); + expect(result).toBe("a/\nb"); + }); }); describe("createReadFileTool", () => { - it("reads a real temp file", async () => { - const filePath = join(workdir, "hello.txt"); - await writeFile(filePath, "hello\nworld\n", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "hello.txt" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("1: hello"); - expect(result.content).toContain("2: world"); - }); - - it("respects offset and limit", async () => { - const filePath = join(workdir, "lines.txt"); - await writeFile(filePath, "a\nb\nc\nd\ne\n", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "lines.txt", offset: 2, limit: 2 }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("2: b\n3: c"); - }); - - it("returns error for missing file", async () => { - const tool = makeTool(workdir); - const result = await tool.execute({ path: "nonexistent.txt" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("not found"); - }); - - it("returns empty-file content for empty file", async () => { - const filePath = join(workdir, "empty.txt"); - await writeFile(filePath, "", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "empty.txt" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("empty file"); - expect(result.content).toContain("empty.txt"); - }); - - it("returns error for offset beyond file length", async () => { - const filePath = join(workdir, "short.txt"); - await writeFile(filePath, "one\n", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "short.txt", offset: 100 }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("exceeds total lines"); - }); - - it("never throws on bad input (always returns ToolResult)", async () => { - const tool = makeTool(workdir); - - const inputs = [null, undefined, 42, "string", {}, { path: "" }, { path: 123 }]; - for (const input of inputs) { - const result = await tool.execute(input, stubCtx()); - expect(result).toHaveProperty("content"); - expect(typeof result.content).toBe("string"); - } - }); - - it("concurrencySafe is true", () => { - const tool = makeTool(workdir); - expect(tool.concurrencySafe).toBe(true); - }); - - it("has correct name and parameters shape", () => { - const tool = makeTool(workdir); - expect(tool.name).toBe("read_file"); - expect(tool.parameters.type).toBe("object"); - expect(tool.parameters.required).toEqual(["path"]); - expect(tool.parameters.properties?.path?.type).toBe("string"); - }); - - it("reads file under ctx.cwd when set (not baked workdir)", async () => { - const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-")); - try { - const filePath = join(ctxDir, "ctx-file.txt"); - await writeFile(filePath, "from ctx cwd", "utf8"); - - const tool = makeTool(workdir); // baked workdir is different - const result = await tool.execute({ path: "ctx-file.txt" }, stubCtx({ cwd: ctxDir })); - - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("1: from ctx cwd"); - } finally { - await rm(ctxDir, { recursive: true, force: true }); - } - }); - - it("falls back to baked workdir when ctx.cwd is omitted", async () => { - const filePath = join(workdir, "baked-file.txt"); - await writeFile(filePath, "from baked workdir", "utf8"); - - const tool = makeTool(workdir); - const ctx = stubCtx(); - // Ensure cwd is undefined - expect(ctx.cwd).toBeUndefined(); - const result = await tool.execute({ path: "baked-file.txt" }, ctx); - - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("1: from baked workdir"); - }); - - it("lists directory entries sorted with trailing slash on subdirectories", async () => { - await mkdir(join(workdir, "subdir")); - await writeFile(join(workdir, "zebra.txt"), "z", "utf8"); - await writeFile(join(workdir, "alpha.txt"), "a", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "." }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("alpha.txt\nsubdir/\nzebra.txt"); - }); - - it("returns empty-directory message for an empty dir", async () => { - await mkdir(join(workdir, "empty-dir")); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "empty-dir" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("(empty directory: empty-dir)"); - }); - - it("reads a file unchanged (regression: line numbers + offset/limit)", async () => { - await writeFile(join(workdir, "regression.txt"), "a\nb\nc\nd\ne\n", "utf8"); - - const tool = makeTool(workdir); - const result = await tool.execute({ path: "regression.txt", offset: 2, limit: 3 }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("2: b\n3: c\n4: d"); - }); - - it("returns not-found for a nonexistent path", async () => { - const tool = makeTool(workdir); - const result = await tool.execute({ path: "nonexistent-path" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toContain("not found"); - }); - - it("routes fs calls through resolveBackend(ctx.computerId) (transport seam)", async () => { - // A fake backend records what it is asked to do. Proves the tool programs - // against the ExecBackend surface (not node:fs) and that the resolver is - // invoked with ctx.computerId — the SSH seam. No real fs involved. - let statCalls = 0; - let readFileCalls = 0; - let readdirCalls = 0; - let receivedComputerId: string | undefined = "__sentinel__"; - const fakeBackend = { - spawn: async () => ({ exitCode: 0, timedOut: false, aborted: false }), - readFile: async (path: string) => { - readFileCalls++; - expect(path).toContain("seam.txt"); - return "fake-line-1\nfake-line-2"; - }, - writeFile: async () => {}, - stat: async (path: string) => { - statCalls++; - expect(path).toContain("seam.txt"); - return { isFile: true, isDirectory: false }; - }, - readdir: async () => { - readdirCalls++; - return []; - }, - exists: async () => true, - } as const; - - const tool = createReadFileTool({ - resolveBackend: (computerId) => { - receivedComputerId = computerId; - return fakeBackend; - }, - workdir, - }); - const result = await tool.execute({ path: "seam.txt" }, stubCtx({ computerId: "prod-ssh" })); - - expect(receivedComputerId).toBe("prod-ssh"); - expect(statCalls).toBe(1); - expect(readFileCalls).toBe(1); - expect(readdirCalls).toBe(0); - expect(result.isError).toBeUndefined(); - expect(result.content).toBe("1: fake-line-1\n2: fake-line-2"); - }); - - it("resolves the local backend when ctx.computerId is undefined (backward compat)", async () => { - // computerId undefined → resolver returns localExecBackend → real fs. - const tool = createReadFileTool({ resolveBackend: () => localExecBackend, workdir }); - const filePath = join(workdir, "compat.txt"); - await writeFile(filePath, "real fs via backend\n", "utf8"); - - const result = await tool.execute({ path: "compat.txt" }, stubCtx()); - - expect(result.isError).toBeUndefined(); - expect(result.content).toContain("1: real fs via backend"); - }); - - it("preserves ENOENT .code branch through the backend (fake backend throws)", async () => { - const enoent = Object.assign(new Error("ENOENT: no such file or directory"), { - code: "ENOENT", - }); - const fakeBackend = { - spawn: async () => ({ exitCode: 0, timedOut: false, aborted: false }), - readFile: async () => "unused", - writeFile: async () => {}, - stat: async () => { - throw enoent; - }, - readdir: async () => [], - exists: async () => false, - } as const; - - const tool = createReadFileTool({ resolveBackend: () => fakeBackend, workdir }); - const result = await tool.execute({ path: "ghost.txt" }, stubCtx()); - - expect(result.isError).toBe(true); - expect(result.content).toBe('Error: File "ghost.txt" not found.'); - }); + it("reads a real temp file", async () => { + const filePath = join(workdir, "hello.txt"); + await writeFile(filePath, "hello\nworld\n", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "hello.txt" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: hello"); + expect(result.content).toContain("2: world"); + }); + + it("respects offset and limit", async () => { + const filePath = join(workdir, "lines.txt"); + await writeFile(filePath, "a\nb\nc\nd\ne\n", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "lines.txt", offset: 2, limit: 2 }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("2: b\n3: c"); + }); + + it("returns error for missing file", async () => { + const tool = makeTool(workdir); + const result = await tool.execute({ path: "nonexistent.txt" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("not found"); + }); + + it("returns empty-file content for empty file", async () => { + const filePath = join(workdir, "empty.txt"); + await writeFile(filePath, "", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "empty.txt" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("empty file"); + expect(result.content).toContain("empty.txt"); + }); + + it("returns error for offset beyond file length", async () => { + const filePath = join(workdir, "short.txt"); + await writeFile(filePath, "one\n", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "short.txt", offset: 100 }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("exceeds total lines"); + }); + + it("never throws on bad input (always returns ToolResult)", async () => { + const tool = makeTool(workdir); + + const inputs = [null, undefined, 42, "string", {}, { path: "" }, { path: 123 }]; + for (const input of inputs) { + const result = await tool.execute(input, stubCtx()); + expect(result).toHaveProperty("content"); + expect(typeof result.content).toBe("string"); + } + }); + + it("concurrencySafe is true", () => { + const tool = makeTool(workdir); + expect(tool.concurrencySafe).toBe(true); + }); + + it("has correct name and parameters shape", () => { + const tool = makeTool(workdir); + expect(tool.name).toBe("read_file"); + expect(tool.parameters.type).toBe("object"); + expect(tool.parameters.required).toEqual(["path"]); + expect(tool.parameters.properties?.path?.type).toBe("string"); + }); + + it("reads file under ctx.cwd when set (not baked workdir)", async () => { + const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-")); + try { + const filePath = join(ctxDir, "ctx-file.txt"); + await writeFile(filePath, "from ctx cwd", "utf8"); + + const tool = makeTool(workdir); // baked workdir is different + const result = await tool.execute({ path: "ctx-file.txt" }, stubCtx({ cwd: ctxDir })); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: from ctx cwd"); + } finally { + await rm(ctxDir, { recursive: true, force: true }); + } + }); + + it("falls back to baked workdir when ctx.cwd is omitted", async () => { + const filePath = join(workdir, "baked-file.txt"); + await writeFile(filePath, "from baked workdir", "utf8"); + + const tool = makeTool(workdir); + const ctx = stubCtx(); + // Ensure cwd is undefined + expect(ctx.cwd).toBeUndefined(); + const result = await tool.execute({ path: "baked-file.txt" }, ctx); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: from baked workdir"); + }); + + it("lists directory entries sorted with trailing slash on subdirectories", async () => { + await mkdir(join(workdir, "subdir")); + await writeFile(join(workdir, "zebra.txt"), "z", "utf8"); + await writeFile(join(workdir, "alpha.txt"), "a", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "." }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("alpha.txt\nsubdir/\nzebra.txt"); + }); + + it("returns empty-directory message for an empty dir", async () => { + await mkdir(join(workdir, "empty-dir")); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "empty-dir" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("(empty directory: empty-dir)"); + }); + + it("reads a file unchanged (regression: line numbers + offset/limit)", async () => { + await writeFile(join(workdir, "regression.txt"), "a\nb\nc\nd\ne\n", "utf8"); + + const tool = makeTool(workdir); + const result = await tool.execute({ path: "regression.txt", offset: 2, limit: 3 }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("2: b\n3: c\n4: d"); + }); + + it("returns not-found for a nonexistent path", async () => { + const tool = makeTool(workdir); + const result = await tool.execute({ path: "nonexistent-path" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toContain("not found"); + }); + + it("routes fs calls through resolveBackend(ctx.computerId) (transport seam)", async () => { + // A fake backend records what it is asked to do. Proves the tool programs + // against the ExecBackend surface (not node:fs) and that the resolver is + // invoked with ctx.computerId — the SSH seam. No real fs involved. + let statCalls = 0; + let readFileCalls = 0; + let readdirCalls = 0; + let receivedComputerId: string | undefined = "__sentinel__"; + const fakeBackend = { + spawn: async () => ({ exitCode: 0, timedOut: false, aborted: false }), + readFile: async (path: string) => { + readFileCalls++; + expect(path).toContain("seam.txt"); + return "fake-line-1\nfake-line-2"; + }, + writeFile: async () => {}, + stat: async (path: string) => { + statCalls++; + expect(path).toContain("seam.txt"); + return { isFile: true, isDirectory: false }; + }, + readdir: async () => { + readdirCalls++; + return []; + }, + exists: async () => true, + } as const; + + const tool = createReadFileTool({ + resolveBackend: (computerId) => { + receivedComputerId = computerId; + return fakeBackend; + }, + workdir, + }); + const result = await tool.execute({ path: "seam.txt" }, stubCtx({ computerId: "prod-ssh" })); + + expect(receivedComputerId).toBe("prod-ssh"); + expect(statCalls).toBe(1); + expect(readFileCalls).toBe(1); + expect(readdirCalls).toBe(0); + expect(result.isError).toBeUndefined(); + expect(result.content).toBe("1: fake-line-1\n2: fake-line-2"); + }); + + it("resolves the local backend when ctx.computerId is undefined (backward compat)", async () => { + // computerId undefined → resolver returns localExecBackend → real fs. + const tool = createReadFileTool({ resolveBackend: () => localExecBackend, workdir }); + const filePath = join(workdir, "compat.txt"); + await writeFile(filePath, "real fs via backend\n", "utf8"); + + const result = await tool.execute({ path: "compat.txt" }, stubCtx()); + + expect(result.isError).toBeUndefined(); + expect(result.content).toContain("1: real fs via backend"); + }); + + it("preserves ENOENT .code branch through the backend (fake backend throws)", async () => { + const enoent = Object.assign(new Error("ENOENT: no such file or directory"), { + code: "ENOENT", + }); + const fakeBackend = { + spawn: async () => ({ exitCode: 0, timedOut: false, aborted: false }), + readFile: async () => "unused", + writeFile: async () => {}, + stat: async () => { + throw enoent; + }, + readdir: async () => [], + exists: async () => false, + } as const; + + const tool = createReadFileTool({ resolveBackend: () => fakeBackend, workdir }); + const result = await tool.execute({ path: "ghost.txt" }, stubCtx()); + + expect(result.isError).toBe(true); + expect(result.content).toBe('Error: File "ghost.txt" not found.'); + }); }); |
