ripgrep.test.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { describe, expect } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { Effect } from "effect"
  5. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  6. import { Ripgrep } from "@opencode-ai/core/ripgrep"
  7. import { RelativePath } from "@opencode-ai/core/schema"
  8. import { tmpdir } from "./fixture/tmpdir"
  9. import { testEffect } from "./lib/effect"
  10. const it = testEffect(LayerNode.compile(Ripgrep.node))
  11. describe("Ripgrep", () => {
  12. it.live("globs files as an array", () =>
  13. Effect.acquireUseRelease(
  14. Effect.promise(() => tmpdir()),
  15. (tmp) =>
  16. Effect.gen(function* () {
  17. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
  18. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n"))
  19. const result = yield* (yield* Ripgrep.Service).glob({ cwd: tmp.path, pattern: "**/*.ts", limit: 10 })
  20. expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")])
  21. }),
  22. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  23. ),
  24. )
  25. it.live("greps files with include filtering", () =>
  26. Effect.acquireUseRelease(
  27. Effect.promise(() => tmpdir()),
  28. (tmp) =>
  29. Effect.gen(function* () {
  30. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
  31. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "match.ts"), "needle\n"))
  32. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "skip.txt"), "needle\n"))
  33. const result = yield* (yield* Ripgrep.Service).grep({
  34. cwd: tmp.path,
  35. pattern: "needle",
  36. include: "*.ts",
  37. limit: 10,
  38. })
  39. expect(result).toHaveLength(1)
  40. expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts"))
  41. expect(result[0]?.submatches[0]?.text).toBe("needle")
  42. }),
  43. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  44. ),
  45. )
  46. it.live("keeps ignored files out of catch-all find results", () =>
  47. Effect.acquireUseRelease(
  48. Effect.promise(() => tmpdir()),
  49. (tmp) =>
  50. Effect.gen(function* () {
  51. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true }))
  52. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
  53. yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`)
  54. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n"))
  55. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n"))
  56. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n"))
  57. const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 })
  58. expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js"))
  59. expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js"))
  60. }),
  61. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  62. ),
  63. )
  64. it.live("never includes git metadata", () =>
  65. Effect.acquireUseRelease(
  66. Effect.promise(() => tmpdir()),
  67. (tmp) =>
  68. Effect.gen(function* () {
  69. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".opencode")))
  70. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".opencode", "config"), "needle\n"))
  71. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".git")))
  72. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".git", "config"), "needle\n"))
  73. const ripgrep = yield* Ripgrep.Service
  74. const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 })
  75. expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
  76. expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config"))
  77. const observed: string[] = []
  78. const limited = yield* ripgrep.find({
  79. cwd: tmp.path,
  80. pattern: "**/*",
  81. limit: 1,
  82. onEntry: (entry) => Effect.sync(() => observed.push(entry.path)),
  83. })
  84. expect(observed).toEqual(limited.map((item) => item.path))
  85. const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
  86. expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
  87. expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config"))
  88. }),
  89. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  90. ),
  91. )
  92. it.live("excludes protected directory trees from catch-all find results", () =>
  93. Effect.acquireUseRelease(
  94. Effect.promise(() => tmpdir()),
  95. (tmp) =>
  96. Effect.gen(function* () {
  97. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "Pictures")))
  98. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "Pictures", "private.jpg"), "private\n"))
  99. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "visible.txt"), "visible\n"))
  100. const files = yield* (yield* Ripgrep.Service).find({
  101. cwd: tmp.path,
  102. pattern: "*",
  103. limit: 10,
  104. exclude: ["Pictures/**"],
  105. })
  106. expect(files.map((item) => item.path)).toContain(RelativePath.make("visible.txt"))
  107. expect(files.map((item) => item.path)).not.toContain(RelativePath.make("Pictures/private.jpg"))
  108. }),
  109. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  110. ),
  111. )
  112. it.live("returns a bounded preview for matches on oversized lines", () =>
  113. Effect.acquireUseRelease(
  114. Effect.promise(() => tmpdir()),
  115. (tmp) =>
  116. Effect.gen(function* () {
  117. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "generated.ts"), `Cloudflare${"x".repeat(70 * 1024)}\n`))
  118. const matches = yield* (yield* Ripgrep.Service).grep({
  119. cwd: tmp.path,
  120. pattern: "Cloudflare",
  121. limit: 10,
  122. })
  123. expect(matches).toHaveLength(1)
  124. expect(matches[0]?.entry.path).toBe(RelativePath.make("generated.ts"))
  125. expect(matches[0]?.text).toHaveLength(2_003)
  126. expect(matches[0]?.text.endsWith("...")).toBe(true)
  127. expect(matches[0]?.submatches).toEqual([{ text: "Cloudflare", start: 0, end: 10 }])
  128. }),
  129. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  130. ),
  131. )
  132. })