read.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { ReadTool } from "../../src/tool/read"
  4. import { Instance } from "../../src/project/instance"
  5. import { tmpdir } from "../fixture/fixture"
  6. import type { PermissionNext } from "../../src/permission/next"
  7. const ctx = {
  8. sessionID: "test",
  9. messageID: "",
  10. callID: "",
  11. agent: "build",
  12. abort: AbortSignal.any([]),
  13. metadata: () => {},
  14. ask: async () => {},
  15. }
  16. describe("tool.read external_directory permission", () => {
  17. test("allows reading absolute path inside project directory", async () => {
  18. await using tmp = await tmpdir({
  19. init: async (dir) => {
  20. await Bun.write(path.join(dir, "test.txt"), "hello world")
  21. },
  22. })
  23. await Instance.provide({
  24. directory: tmp.path,
  25. fn: async () => {
  26. const read = await ReadTool.init()
  27. const result = await read.execute({ filePath: path.join(tmp.path, "test.txt") }, ctx)
  28. expect(result.output).toContain("hello world")
  29. },
  30. })
  31. })
  32. test("allows reading file in subdirectory inside project directory", async () => {
  33. await using tmp = await tmpdir({
  34. init: async (dir) => {
  35. await Bun.write(path.join(dir, "subdir", "test.txt"), "nested content")
  36. },
  37. })
  38. await Instance.provide({
  39. directory: tmp.path,
  40. fn: async () => {
  41. const read = await ReadTool.init()
  42. const result = await read.execute({ filePath: path.join(tmp.path, "subdir", "test.txt") }, ctx)
  43. expect(result.output).toContain("nested content")
  44. },
  45. })
  46. })
  47. test("asks for external_directory permission when reading absolute path outside project", async () => {
  48. await using outerTmp = await tmpdir({
  49. init: async (dir) => {
  50. await Bun.write(path.join(dir, "secret.txt"), "secret data")
  51. },
  52. })
  53. await using tmp = await tmpdir({ git: true })
  54. await Instance.provide({
  55. directory: tmp.path,
  56. fn: async () => {
  57. const read = await ReadTool.init()
  58. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  59. const testCtx = {
  60. ...ctx,
  61. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  62. requests.push(req)
  63. },
  64. }
  65. await read.execute({ filePath: path.join(outerTmp.path, "secret.txt") }, testCtx)
  66. const extDirReq = requests.find((r) => r.permission === "external_directory")
  67. expect(extDirReq).toBeDefined()
  68. expect(extDirReq!.patterns.some((p) => p.includes(outerTmp.path))).toBe(true)
  69. },
  70. })
  71. })
  72. test("asks for external_directory permission when reading relative path outside project", async () => {
  73. await using tmp = await tmpdir({ git: true })
  74. await Instance.provide({
  75. directory: tmp.path,
  76. fn: async () => {
  77. const read = await ReadTool.init()
  78. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  79. const testCtx = {
  80. ...ctx,
  81. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  82. requests.push(req)
  83. },
  84. }
  85. // This will fail because file doesn't exist, but we can check if permission was asked
  86. await read.execute({ filePath: "../outside.txt" }, testCtx).catch(() => {})
  87. const extDirReq = requests.find((r) => r.permission === "external_directory")
  88. expect(extDirReq).toBeDefined()
  89. },
  90. })
  91. })
  92. test("does not ask for external_directory permission when reading inside project", async () => {
  93. await using tmp = await tmpdir({
  94. git: true,
  95. init: async (dir) => {
  96. await Bun.write(path.join(dir, "internal.txt"), "internal content")
  97. },
  98. })
  99. await Instance.provide({
  100. directory: tmp.path,
  101. fn: async () => {
  102. const read = await ReadTool.init()
  103. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  104. const testCtx = {
  105. ...ctx,
  106. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  107. requests.push(req)
  108. },
  109. }
  110. await read.execute({ filePath: path.join(tmp.path, "internal.txt") }, testCtx)
  111. const extDirReq = requests.find((r) => r.permission === "external_directory")
  112. expect(extDirReq).toBeUndefined()
  113. },
  114. })
  115. })
  116. })
  117. describe("tool.read env file blocking", () => {
  118. test.each([
  119. [".env", true],
  120. [".env.local", true],
  121. [".env.production", true],
  122. [".env.sample", false],
  123. [".env.example", false],
  124. [".envrc", false],
  125. ["environment.ts", false],
  126. ])("%s blocked=%s", async (filename, blocked) => {
  127. await using tmp = await tmpdir({
  128. init: (dir) => Bun.write(path.join(dir, filename), "content"),
  129. })
  130. await Instance.provide({
  131. directory: tmp.path,
  132. fn: async () => {
  133. const read = await ReadTool.init()
  134. const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctx)
  135. if (blocked) {
  136. await expect(promise).rejects.toThrow("blocked")
  137. } else {
  138. expect((await promise).output).toContain("content")
  139. }
  140. },
  141. })
  142. })
  143. })