| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- import { describe, expect, test } from "bun:test"
- import { Effect } from "effect"
- import * as Stream from "effect/Stream"
- import fs from "fs/promises"
- import path from "path"
- import { tmpdir } from "../fixture/fixture"
- import { Ripgrep } from "../../src/file/ripgrep"
- async function seed(dir: string, count: number, size = 16) {
- const txt = "a".repeat(size)
- await Promise.all(Array.from({ length: count }, (_, i) => Bun.write(path.join(dir, `file-${i}.txt`), `${txt}${i}\n`)))
- }
- function env(name: string, value: string | undefined) {
- const prev = process.env[name]
- if (value === undefined) delete process.env[name]
- else process.env[name] = value
- return () => {
- if (prev === undefined) delete process.env[name]
- else process.env[name] = prev
- }
- }
- describe("file.ripgrep", () => {
- test("defaults to include hidden", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "visible.txt"), "hello")
- await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
- await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}")
- },
- })
- const files = await Array.fromAsync(await Ripgrep.files({ cwd: tmp.path }))
- expect(files.includes("visible.txt")).toBe(true)
- expect(files.includes(path.join(".opencode", "thing.json"))).toBe(true)
- })
- test("hidden false excludes hidden", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "visible.txt"), "hello")
- await fs.mkdir(path.join(dir, ".opencode"), { recursive: true })
- await Bun.write(path.join(dir, ".opencode", "thing.json"), "{}")
- },
- })
- const files = await Array.fromAsync(await Ripgrep.files({ cwd: tmp.path, hidden: false }))
- expect(files.includes("visible.txt")).toBe(true)
- expect(files.includes(path.join(".opencode", "thing.json"))).toBe(false)
- })
- // kilocode_change start - .kilo directory should also be skipped in tree()
- test("tree skips .kilo directory files", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "src", "main.ts"), "export {}")
- await fs.mkdir(path.join(dir, ".kilo"), { recursive: true })
- await Bun.write(path.join(dir, ".kilo", "config.json"), "{}")
- },
- })
- const result = await Ripgrep.tree({ cwd: tmp.path })
- expect(result).not.toContain(".kilo")
- expect(result).toContain("src")
- })
- // kilocode_change end
- test("search returns empty when nothing matches", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "match.ts"), "const value = 'other'\n")
- },
- })
- const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" })
- expect(result.partial).toBe(false)
- expect(result.items).toEqual([])
- })
- test("search returns match metadata with normalized path", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await fs.mkdir(path.join(dir, "src"), { recursive: true })
- await Bun.write(path.join(dir, "src", "match.ts"), "const needle = 1\n")
- },
- })
- const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" })
- expect(result.partial).toBe(false)
- expect(result.items).toHaveLength(1)
- expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts"))
- expect(result.items[0]?.line_number).toBe(1)
- expect(result.items[0]?.lines.text).toContain("needle")
- })
- test("files returns empty when glob matches no files in worker mode", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await fs.mkdir(path.join(dir, "packages", "console"), { recursive: true })
- await Bun.write(path.join(dir, "packages", "console", "package.json"), "{}")
- },
- })
- const ctl = new AbortController()
- const files = await Array.fromAsync(
- await Ripgrep.files({
- cwd: tmp.path,
- glob: ["packages/*"],
- signal: ctl.signal,
- }),
- )
- expect(files).toEqual([])
- })
- test("ignores RIPGREP_CONFIG_PATH in direct mode", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n")
- },
- })
- const restore = env("RIPGREP_CONFIG_PATH", path.join(tmp.path, "missing-ripgreprc"))
- try {
- const result = await Ripgrep.search({ cwd: tmp.path, pattern: "needle" })
- expect(result.items).toHaveLength(1)
- } finally {
- restore()
- }
- })
- test("ignores RIPGREP_CONFIG_PATH in worker mode", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "match.ts"), "const needle = 1\n")
- },
- })
- const restore = env("RIPGREP_CONFIG_PATH", path.join(tmp.path, "missing-ripgreprc"))
- try {
- const ctl = new AbortController()
- const result = await Ripgrep.search({
- cwd: tmp.path,
- pattern: "needle",
- signal: ctl.signal,
- })
- expect(result.items).toHaveLength(1)
- } finally {
- restore()
- }
- })
- test("aborts files scan in worker mode", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await seed(dir, 4000)
- },
- })
- const ctl = new AbortController()
- const iter = await Ripgrep.files({ cwd: tmp.path, signal: ctl.signal })
- const pending = Array.fromAsync(iter)
- setTimeout(() => ctl.abort(), 0)
- const err = await pending.catch((err) => err)
- expect(err).toBeInstanceOf(Error)
- expect(err.name).toBe("AbortError")
- }, 15_000)
- test("aborts search in worker mode", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await seed(dir, 512, 64 * 1024)
- },
- })
- const ctl = new AbortController()
- const pending = Ripgrep.search({ cwd: tmp.path, pattern: "needle", signal: ctl.signal })
- setTimeout(() => ctl.abort(), 0)
- const err = await pending.catch((err) => err)
- expect(err).toBeInstanceOf(Error)
- expect(err.name).toBe("AbortError")
- }, 15_000)
- })
- describe("Ripgrep.Service", () => {
- test("search returns matched rows", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "match.ts"), "const value = 'needle'\n")
- await Bun.write(path.join(dir, "skip.txt"), "const value = 'other'\n")
- },
- })
- const result = await Effect.gen(function* () {
- const rg = yield* Ripgrep.Service
- return yield* rg.search({ cwd: tmp.path, pattern: "needle", glob: ["*.ts"] })
- }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
- expect(result.partial).toBe(false)
- expect(result.items).toHaveLength(1)
- expect(result.items[0]?.path.text).toContain("match.ts")
- expect(result.items[0]?.lines.text).toContain("needle")
- })
- test("search supports explicit file targets", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "match.ts"), "const value = 'needle'\n")
- await Bun.write(path.join(dir, "skip.ts"), "const value = 'needle'\n")
- },
- })
- const file = path.join(tmp.path, "match.ts")
- const result = await Effect.gen(function* () {
- const rg = yield* Ripgrep.Service
- return yield* rg.search({ cwd: tmp.path, pattern: "needle", file: [file] })
- }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
- expect(result.partial).toBe(false)
- expect(result.items).toHaveLength(1)
- expect(result.items[0]?.path.text).toBe(file)
- })
- test("files returns stream of filenames", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "a.txt"), "hello")
- await Bun.write(path.join(dir, "b.txt"), "world")
- },
- })
- const files = await Effect.gen(function* () {
- const rg = yield* Ripgrep.Service
- return yield* rg.files({ cwd: tmp.path }).pipe(
- Stream.runCollect,
- Effect.map((chunk) => [...chunk].sort()),
- )
- }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
- expect(files).toEqual(["a.txt", "b.txt"])
- })
- test("files respects glob filter", async () => {
- await using tmp = await tmpdir({
- init: async (dir) => {
- await Bun.write(path.join(dir, "keep.ts"), "yes")
- await Bun.write(path.join(dir, "skip.txt"), "no")
- },
- })
- const files = await Effect.gen(function* () {
- const rg = yield* Ripgrep.Service
- return yield* rg.files({ cwd: tmp.path, glob: ["*.ts"] }).pipe(
- Stream.runCollect,
- Effect.map((chunk) => [...chunk]),
- )
- }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromise)
- expect(files).toEqual(["keep.ts"])
- })
- test("files dies on nonexistent directory", async () => {
- const exit = await Effect.gen(function* () {
- const rg = yield* Ripgrep.Service
- return yield* rg.files({ cwd: "/tmp/nonexistent-dir-12345" }).pipe(Stream.runCollect)
- }).pipe(Effect.provide(Ripgrep.defaultLayer), Effect.runPromiseExit)
- expect(exit._tag).toBe("Failure")
- })
- })
|