ripgrep.test.ts 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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("keeps ignored files out of catch-all find results", () =>
  13. Effect.acquireUseRelease(
  14. Effect.promise(() => tmpdir()),
  15. (tmp) =>
  16. Effect.gen(function* () {
  17. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true }))
  18. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src")))
  19. yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`)
  20. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n"))
  21. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n"))
  22. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n"))
  23. const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 })
  24. expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js"))
  25. expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js"))
  26. }),
  27. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  28. ),
  29. )
  30. it.live("never includes git metadata", () =>
  31. Effect.acquireUseRelease(
  32. Effect.promise(() => tmpdir()),
  33. (tmp) =>
  34. Effect.gen(function* () {
  35. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".opencode")))
  36. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".opencode", "config"), "needle\n"))
  37. yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, ".git")))
  38. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".git", "config"), "needle\n"))
  39. const ripgrep = yield* Ripgrep.Service
  40. const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 })
  41. expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config"))
  42. expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config"))
  43. const observed: string[] = []
  44. const limited = yield* ripgrep.find({
  45. cwd: tmp.path,
  46. pattern: "**/*",
  47. limit: 1,
  48. onEntry: (entry) => Effect.sync(() => observed.push(entry.path)),
  49. })
  50. expect(observed).toEqual(limited.map((item) => item.path))
  51. const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 })
  52. expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config"))
  53. expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config"))
  54. }),
  55. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  56. ),
  57. )
  58. it.live("returns a bounded preview for matches on oversized lines", () =>
  59. Effect.acquireUseRelease(
  60. Effect.promise(() => tmpdir()),
  61. (tmp) =>
  62. Effect.gen(function* () {
  63. yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "generated.ts"), `Cloudflare${"x".repeat(70 * 1024)}\n`))
  64. const matches = yield* (yield* Ripgrep.Service).grep({
  65. cwd: tmp.path,
  66. pattern: "Cloudflare",
  67. limit: 10,
  68. })
  69. expect(matches).toHaveLength(1)
  70. expect(matches[0]?.entry.path).toBe(RelativePath.make("generated.ts"))
  71. expect(matches[0]?.text).toHaveLength(2_003)
  72. expect(matches[0]?.text.endsWith("...")).toBe(true)
  73. expect(matches[0]?.submatches).toEqual([{ text: "Cloudflare", start: 0, end: 10 }])
  74. }),
  75. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  76. ),
  77. )
  78. })