bash.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { BashTool } from "../../src/tool/bash"
  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. const projectRoot = path.join(__dirname, "../..")
  17. describe("tool.bash", () => {
  18. test("basic", async () => {
  19. await Instance.provide({
  20. directory: projectRoot,
  21. fn: async () => {
  22. const bash = await BashTool.init()
  23. const result = await bash.execute(
  24. {
  25. command: "echo 'test'",
  26. description: "Echo test message",
  27. },
  28. ctx,
  29. )
  30. expect(result.metadata.exit).toBe(0)
  31. expect(result.metadata.output).toContain("test")
  32. },
  33. })
  34. })
  35. })
  36. describe("tool.bash permissions", () => {
  37. test("asks for bash permission with correct pattern", async () => {
  38. await using tmp = await tmpdir({ git: true })
  39. await Instance.provide({
  40. directory: tmp.path,
  41. fn: async () => {
  42. const bash = await BashTool.init()
  43. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  44. const testCtx = {
  45. ...ctx,
  46. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  47. requests.push(req)
  48. },
  49. }
  50. await bash.execute(
  51. {
  52. command: "echo hello",
  53. description: "Echo hello",
  54. },
  55. testCtx,
  56. )
  57. expect(requests.length).toBe(1)
  58. expect(requests[0].permission).toBe("bash")
  59. expect(requests[0].patterns).toContain("echo hello")
  60. },
  61. })
  62. })
  63. test("asks for bash permission with multiple commands", async () => {
  64. await using tmp = await tmpdir({ git: true })
  65. await Instance.provide({
  66. directory: tmp.path,
  67. fn: async () => {
  68. const bash = await BashTool.init()
  69. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  70. const testCtx = {
  71. ...ctx,
  72. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  73. requests.push(req)
  74. },
  75. }
  76. await bash.execute(
  77. {
  78. command: "echo foo && echo bar",
  79. description: "Echo twice",
  80. },
  81. testCtx,
  82. )
  83. expect(requests.length).toBe(1)
  84. expect(requests[0].permission).toBe("bash")
  85. expect(requests[0].patterns).toContain("echo foo")
  86. expect(requests[0].patterns).toContain("echo bar")
  87. },
  88. })
  89. })
  90. test("asks for external_directory permission when cd to parent", async () => {
  91. await using tmp = await tmpdir({ git: true })
  92. await Instance.provide({
  93. directory: tmp.path,
  94. fn: async () => {
  95. const bash = await BashTool.init()
  96. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  97. const testCtx = {
  98. ...ctx,
  99. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  100. requests.push(req)
  101. },
  102. }
  103. await bash.execute(
  104. {
  105. command: "cd ../",
  106. description: "Change to parent directory",
  107. },
  108. testCtx,
  109. )
  110. const extDirReq = requests.find((r) => r.permission === "external_directory")
  111. expect(extDirReq).toBeDefined()
  112. },
  113. })
  114. })
  115. test("asks for external_directory permission when workdir is outside project", async () => {
  116. await using tmp = await tmpdir({ git: true })
  117. await Instance.provide({
  118. directory: tmp.path,
  119. fn: async () => {
  120. const bash = await BashTool.init()
  121. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  122. const testCtx = {
  123. ...ctx,
  124. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  125. requests.push(req)
  126. },
  127. }
  128. await bash.execute(
  129. {
  130. command: "ls",
  131. workdir: "/tmp",
  132. description: "List /tmp",
  133. },
  134. testCtx,
  135. )
  136. const extDirReq = requests.find((r) => r.permission === "external_directory")
  137. expect(extDirReq).toBeDefined()
  138. expect(extDirReq!.patterns).toContain("/tmp")
  139. },
  140. })
  141. })
  142. test("does not ask for external_directory permission when rm inside project", async () => {
  143. await using tmp = await tmpdir({ git: true })
  144. await Instance.provide({
  145. directory: tmp.path,
  146. fn: async () => {
  147. const bash = await BashTool.init()
  148. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  149. const testCtx = {
  150. ...ctx,
  151. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  152. requests.push(req)
  153. },
  154. }
  155. await Bun.write(path.join(tmp.path, "tmpfile"), "x")
  156. await bash.execute(
  157. {
  158. command: "rm tmpfile",
  159. description: "Remove tmpfile",
  160. },
  161. testCtx,
  162. )
  163. const extDirReq = requests.find((r) => r.permission === "external_directory")
  164. expect(extDirReq).toBeUndefined()
  165. },
  166. })
  167. })
  168. test("includes always patterns for auto-approval", async () => {
  169. await using tmp = await tmpdir({ git: true })
  170. await Instance.provide({
  171. directory: tmp.path,
  172. fn: async () => {
  173. const bash = await BashTool.init()
  174. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  175. const testCtx = {
  176. ...ctx,
  177. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  178. requests.push(req)
  179. },
  180. }
  181. await bash.execute(
  182. {
  183. command: "git log --oneline -5",
  184. description: "Git log",
  185. },
  186. testCtx,
  187. )
  188. expect(requests.length).toBe(1)
  189. expect(requests[0].always.length).toBeGreaterThan(0)
  190. expect(requests[0].always.some((p) => p.endsWith("*"))).toBe(true)
  191. },
  192. })
  193. })
  194. test("does not ask for bash permission when command is cd only", async () => {
  195. await using tmp = await tmpdir({ git: true })
  196. await Instance.provide({
  197. directory: tmp.path,
  198. fn: async () => {
  199. const bash = await BashTool.init()
  200. const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
  201. const testCtx = {
  202. ...ctx,
  203. ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
  204. requests.push(req)
  205. },
  206. }
  207. await bash.execute(
  208. {
  209. command: "cd .",
  210. description: "Stay in current directory",
  211. },
  212. testCtx,
  213. )
  214. const bashReq = requests.find((r) => r.permission === "bash")
  215. expect(bashReq).toBeUndefined()
  216. },
  217. })
  218. })
  219. })