import { describe, expect, test } from "bun:test" import path from "path" import { ReadTool } from "../../src/tool/read" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { PermissionNext } from "../../src/permission/next" import { Agent } from "../../src/agent/agent" const ctx = { sessionID: "test", messageID: "", callID: "", agent: "build", abort: AbortSignal.any([]), metadata: () => {}, ask: async () => {}, } describe("tool.read external_directory permission", () => { test("allows reading absolute path inside project directory", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write(path.join(dir, "test.txt"), "hello world") }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const read = await ReadTool.init() const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx) expect(result.output).toContain("hello world") }, }) }) test("allows reading file in subdirectory inside project directory", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content") }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const read = await ReadTool.init() const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx) expect(result.output).toContain("nested content") }, }) }) test("asks for external_directory permission when reading absolute path outside project", async () => { await using outerTmp = await tmpdir({ init: async (dir) => { await Bun.write(path.join(dir, "secret.txt"), "secret data") }, }) await using tmp = await tmpdir({ git: true }) await Instance.provide({ directory: tmp.path, fn: async () => { const read = await ReadTool.init() const requests: Array> = [] const testCtx = { ...ctx, ask: async (req: Omit) => { requests.push(req) }, } await read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, testCtx) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path))).toBe(true) }, }) }) test("asks for external_directory permission when reading relative path outside project", async () => { await using tmp = await tmpdir({ git: true }) await Instance.provide({ directory: tmp.path, fn: async () => { const read = await ReadTool.init() const requests: Array> = [] const testCtx = { ...ctx, ask: async (req: Omit) => { requests.push(req) }, } // This will fail because file doesn't exist, but we can check if permission was asked await read.execute({ filePath: "../outside.txt" }, testCtx).catch(() => {}) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() }, }) }) test("does not ask for external_directory permission when reading inside project", async () => { await using tmp = await tmpdir({ git: true, init: async (dir) => { await Bun.write(path.join(dir, "internal.txt"), "internal content") }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const read = await ReadTool.init() const requests: Array> = [] const testCtx = { ...ctx, ask: async (req: Omit) => { requests.push(req) }, } await read.execute({ filePath: path.join(tmp.path, "internal.txt") }, testCtx) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeUndefined() }, }) }) }) describe("tool.read env file blocking", () => { const cases: [string, boolean][] = [ [".env", true], [".env.local", true], [".env.production", true], [".env.development.local", true], [".env.example", false], [".envrc", false], ["environment.ts", false], ] describe.each(["build", "plan"])("agent=%s", (agentName) => { test.each(cases)("%s blocked=%s", async (filename, blocked) => { await using tmp = await tmpdir({ init: (dir) => Bun.write(path.join(dir, filename), "content"), }) await Instance.provide({ directory: tmp.path, fn: async () => { const agent = await Agent.get(agentName) const ctxWithPermissions = { ...ctx, ask: async (req: Omit) => { for (const pattern of req.patterns) { const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission) if (rule.action === "deny") { throw new PermissionNext.DeniedError(agent.permission) } } }, } const read = await ReadTool.init() const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions) if (blocked) { await expect(promise).rejects.toThrow(PermissionNext.DeniedError) } else { expect((await promise).output).toContain("content") } }, }) }) }) })