search.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  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 } from "effect"
  6. import { Fff } from "#fff"
  7. import { Search } from "@opencode-ai/core/filesystem/search"
  8. import { testEffect } from "../lib/effect"
  9. const it = testEffect(Search.defaultLayer)
  10. const tmpdir = (init?: (dir: string) => Effect.Effect<void>) =>
  11. Effect.acquireRelease(
  12. Effect.promise(async () => fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "opencode-test-")))),
  13. (dir) =>
  14. Effect.promise(() => fs.rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })).pipe(
  15. Effect.ignore,
  16. ),
  17. ).pipe(Effect.tap((dir) => init?.(dir) ?? Effect.void))
  18. const write = (file: string, data: string) => Effect.promise(() => Bun.write(file, data))
  19. describe("file.search", () => {
  20. it.live("uses fff for Bun-backed grep", () =>
  21. Effect.gen(function* () {
  22. expect(Fff.available()).toBe(true)
  23. const dir = yield* tmpdir()
  24. yield* write(path.join(dir, "src", "match.ts"), "const needle = 1\n")
  25. const search = yield* Search.Service
  26. const result = yield* search.search({ cwd: dir, pattern: "needle", limit: 10 })
  27. expect(result.engine).toBe("fff")
  28. expect(result.items).toHaveLength(1)
  29. expect(result.items[0]?.path.text).toBe("src/match.ts")
  30. }),
  31. )
  32. it.live("keeps fuzzy file abbreviation matches", () =>
  33. Effect.gen(function* () {
  34. expect(Fff.available()).toBe(true)
  35. const dir = yield* tmpdir()
  36. yield* write(path.join(dir, "README.md"), "hello\n")
  37. const search = yield* Search.Service
  38. const results = yield* search.file({ cwd: dir, query: "rdme", limit: 10 })
  39. expect(results).toContain("README.md")
  40. }),
  41. )
  42. it.live("keeps empty file query candidates", () =>
  43. Effect.gen(function* () {
  44. expect(Fff.available()).toBe(true)
  45. const dir = yield* tmpdir()
  46. yield* write(path.join(dir, "README.md"), "hello\n")
  47. yield* write(path.join(dir, "src", "main.ts"), "export const main = true\n")
  48. const search = yield* Search.Service
  49. const results = yield* search.file({ cwd: dir, query: "", limit: 10, kind: "all" })
  50. expect(results).toContain("README.md")
  51. expect(results).toContain("src/")
  52. expect(results).not.toContain("")
  53. }),
  54. )
  55. it.live("keeps paging grep results without an explicit limit", () =>
  56. Effect.gen(function* () {
  57. expect(Fff.available()).toBe(true)
  58. const dir = yield* tmpdir()
  59. yield* write(path.join(dir, "matches.txt"), Array.from({ length: 150 }, (_, idx) => `needle ${idx}\n`).join(""))
  60. const search = yield* Search.Service
  61. const result = yield* search.search({ cwd: dir, pattern: "needle" })
  62. expect(result.items).toHaveLength(150)
  63. }),
  64. )
  65. it.live("uses byte ranges for UTF-8 grep submatches", () =>
  66. Effect.gen(function* () {
  67. expect(Fff.available()).toBe(true)
  68. const dir = yield* tmpdir()
  69. yield* write(path.join(dir, "unicode.txt"), "éneedle\n")
  70. const search = yield* Search.Service
  71. const result = yield* search.search({ cwd: dir, pattern: "needle", limit: 10 })
  72. expect(result.items[0]?.submatches[0]?.match.text).toBe("needle")
  73. }),
  74. )
  75. it.live("post-filters fff grep include matches", () =>
  76. Effect.gen(function* () {
  77. expect(Fff.available()).toBe(true)
  78. const dir = yield* tmpdir()
  79. yield* write(path.join(dir, "src", "match.ts"), "needle\n")
  80. yield* write(path.join(dir, "src", "match.txt"), "needle\n")
  81. const search = yield* Search.Service
  82. const result = yield* search.search({ cwd: dir, pattern: "needle", glob: ["*.ts"], limit: 10 })
  83. expect(result.engine).toBe("fff")
  84. expect(result.items.map((entry) => entry.path.text)).toEqual(["src/match.ts"])
  85. }),
  86. )
  87. it.live("keeps fff grep include no-match results", () =>
  88. Effect.gen(function* () {
  89. expect(Fff.available()).toBe(true)
  90. const dir = yield* tmpdir()
  91. yield* write(path.join(dir, "src", "match.ts"), "needle\n")
  92. const search = yield* Search.Service
  93. const result = yield* search.search({ cwd: dir, pattern: "missing", glob: ["*.ts"], limit: 10 })
  94. expect(result.engine).toBe("fff")
  95. expect(result.items).toEqual([])
  96. }),
  97. )
  98. it.live("post-filters fff glob matches", () =>
  99. Effect.gen(function* () {
  100. expect(Fff.available()).toBe(true)
  101. const dir = yield* tmpdir()
  102. yield* write(path.join(dir, "src", "match.ts"), "export const value = 1\n")
  103. yield* write(path.join(dir, "src", "match.txt"), "hello\n")
  104. const search = yield* Search.Service
  105. const result = yield* search.glob({ cwd: dir, pattern: "**/*.ts", limit: 10 })
  106. expect(result.files).toEqual([path.join(dir, "src", "match.ts")])
  107. }),
  108. )
  109. it.live("tracks an opened file against its originating query", () =>
  110. Effect.gen(function* () {
  111. expect(Fff.available()).toBe(true)
  112. const dir = yield* tmpdir()
  113. yield* write(path.join(dir, "alpha-target-one.ts"), "export const one = 1\n")
  114. yield* write(path.join(dir, "alpha-target-two.ts"), "export const two = 2\n")
  115. const search = yield* Search.Service
  116. const results = yield* search.file({ cwd: dir, query: "alpha target two", limit: 10 })
  117. expect(results).toContain("alpha-target-two.ts")
  118. // open() records the query->file association in fff's history db via the
  119. // live picker. It must resolve a remembered file and run without error.
  120. yield* search.open({ cwd: dir, file: "alpha-target-two.ts" })
  121. }),
  122. )
  123. })