grep.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { describe, expect } from "bun:test"
  2. import fs from "fs/promises"
  3. import os from "os"
  4. import path from "path"
  5. import { Effect, Layer } from "effect"
  6. import { GrepTool } from "../../src/tool/grep"
  7. import { provideInstance, TestInstance } from "../fixture/fixture"
  8. import { SessionID, MessageID } from "../../src/session/schema"
  9. import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
  10. import { Truncate } from "@/tool/truncate"
  11. import { Agent } from "../../src/agent/agent"
  12. import { Ripgrep } from "../../src/file/ripgrep"
  13. import { AppFileSystem } from "@opencode-ai/core/filesystem"
  14. import { testEffect } from "../lib/effect"
  15. import { Reference } from "@/reference/reference"
  16. import { Permission } from "../../src/permission"
  17. import type * as Tool from "../../src/tool/tool"
  18. const it = testEffect(
  19. Layer.mergeAll(
  20. CrossSpawnSpawner.defaultLayer,
  21. AppFileSystem.defaultLayer,
  22. Ripgrep.defaultLayer,
  23. Truncate.defaultLayer,
  24. Agent.defaultLayer,
  25. Reference.defaultLayer,
  26. ),
  27. )
  28. const ctx = {
  29. sessionID: SessionID.make("ses_test"),
  30. messageID: MessageID.make("msg_test"),
  31. callID: "",
  32. agent: "build",
  33. abort: AbortSignal.any([]),
  34. messages: [],
  35. metadata: () => Effect.void,
  36. ask: () => Effect.void,
  37. }
  38. const root = path.join(__dirname, "../..")
  39. describe("tool.grep", () => {
  40. it.live("basic search", () =>
  41. Effect.gen(function* () {
  42. const info = yield* GrepTool
  43. const grep = yield* info.init()
  44. const result = yield* provideInstance(root)(
  45. grep.execute(
  46. {
  47. pattern: "export",
  48. path: path.join(root, "src/tool"),
  49. include: "*.ts",
  50. },
  51. ctx,
  52. ),
  53. )
  54. expect(result.metadata.matches).toBeGreaterThan(0)
  55. expect(result.output).toContain("Found")
  56. }),
  57. )
  58. it.instance("no matches returns correct output", () =>
  59. Effect.gen(function* () {
  60. const test = yield* TestInstance
  61. yield* Effect.promise(() => Bun.write(path.join(test.directory, "test.txt"), "hello world"))
  62. const info = yield* GrepTool
  63. const grep = yield* info.init()
  64. const result = yield* grep.execute(
  65. {
  66. pattern: "xyznonexistentpatternxyz123",
  67. path: test.directory,
  68. },
  69. ctx,
  70. )
  71. expect(result.metadata.matches).toBe(0)
  72. expect(result.output).toBe("No files found")
  73. }),
  74. )
  75. it.instance("finds matches in tmp instance", () =>
  76. Effect.gen(function* () {
  77. const test = yield* TestInstance
  78. yield* Effect.promise(() => Bun.write(path.join(test.directory, "test.txt"), "line1\nline2\nline3"))
  79. const info = yield* GrepTool
  80. const grep = yield* info.init()
  81. const result = yield* grep.execute(
  82. {
  83. pattern: "line",
  84. path: test.directory,
  85. },
  86. ctx,
  87. )
  88. expect(result.metadata.matches).toBeGreaterThan(0)
  89. }),
  90. )
  91. it.instance("supports exact file paths", () =>
  92. Effect.gen(function* () {
  93. const test = yield* TestInstance
  94. const file = path.join(test.directory, "test.txt")
  95. yield* Effect.promise(() => Bun.write(file, "line1\nline2\nline3"))
  96. const info = yield* GrepTool
  97. const grep = yield* info.init()
  98. const result = yield* grep.execute(
  99. {
  100. pattern: "line2",
  101. path: file,
  102. },
  103. ctx,
  104. )
  105. expect(result.metadata.matches).toBe(1)
  106. expect(result.output).toContain(file)
  107. expect(result.output).toContain("Line 2: line2")
  108. }),
  109. )
  110. it.instance("does not ask for external_directory when alias path is allowed", () =>
  111. Effect.gen(function* () {
  112. if (process.platform === "win32") return
  113. yield* TestInstance
  114. const tmp = yield* Effect.acquireRelease(
  115. Effect.promise(() => fs.mkdtemp(path.join(os.tmpdir(), "opencode-grep-alias-"))),
  116. (dir) => Effect.promise(() => fs.rm(dir, { recursive: true, force: true })),
  117. )
  118. const real = path.join(tmp, "real")
  119. const alias = path.join(tmp, "alias")
  120. yield* Effect.promise(() => fs.mkdir(real))
  121. yield* Effect.promise(() => fs.symlink(real, alias, "dir"))
  122. yield* Effect.promise(() => Bun.write(path.join(real, "test.txt"), "needle"))
  123. const ruleset = Permission.fromConfig({
  124. grep: "allow",
  125. external_directory: {
  126. [path.join(alias, "*")]: "allow",
  127. },
  128. })
  129. const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  130. const next: Tool.Context = {
  131. ...ctx,
  132. ask: (req) =>
  133. Effect.sync(() => {
  134. const needsAsk = req.patterns.some(
  135. (pattern) => Permission.evaluate(req.permission, pattern, ruleset).action !== "allow",
  136. )
  137. if (needsAsk) requests.push(req)
  138. }),
  139. }
  140. const info = yield* GrepTool
  141. const grep = yield* info.init()
  142. const result = yield* grep.execute(
  143. {
  144. pattern: "needle",
  145. path: alias,
  146. include: "*.txt",
  147. },
  148. next,
  149. )
  150. expect(result.metadata.matches).toBe(1)
  151. expect(requests.find((req) => req.permission === "external_directory")).toBeUndefined()
  152. }),
  153. )
  154. })