grep.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { Effect, Layer, ManagedRuntime } from "effect"
  4. import { GrepTool } from "../../src/tool/grep"
  5. import { Instance } from "../../src/project/instance"
  6. import { tmpdir } from "../fixture/fixture"
  7. import { SessionID, MessageID } from "../../src/session/schema"
  8. import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
  9. const runtime = ManagedRuntime.make(Layer.mergeAll(CrossSpawnSpawner.defaultLayer))
  10. function initGrep() {
  11. return runtime.runPromise(GrepTool.pipe(Effect.flatMap((info) => Effect.promise(() => info.init()))))
  12. }
  13. const ctx = {
  14. sessionID: SessionID.make("ses_test"),
  15. messageID: MessageID.make(""),
  16. callID: "",
  17. agent: "build",
  18. abort: AbortSignal.any([]),
  19. messages: [],
  20. metadata: () => Effect.void,
  21. ask: () => Effect.void,
  22. }
  23. const projectRoot = path.join(__dirname, "../..")
  24. describe("tool.grep", () => {
  25. test("basic search", async () => {
  26. await Instance.provide({
  27. directory: projectRoot,
  28. fn: async () => {
  29. const grep = await initGrep()
  30. const result = await Effect.runPromise(
  31. grep.execute(
  32. {
  33. pattern: "export",
  34. path: path.join(projectRoot, "src/tool"),
  35. include: "*.ts",
  36. },
  37. ctx,
  38. ),
  39. )
  40. expect(result.metadata.matches).toBeGreaterThan(0)
  41. expect(result.output).toContain("Found")
  42. },
  43. })
  44. })
  45. test("no matches returns correct output", async () => {
  46. await using tmp = await tmpdir({
  47. init: async (dir) => {
  48. await Bun.write(path.join(dir, "test.txt"), "hello world")
  49. },
  50. })
  51. await Instance.provide({
  52. directory: tmp.path,
  53. fn: async () => {
  54. const grep = await initGrep()
  55. const result = await Effect.runPromise(
  56. grep.execute(
  57. {
  58. pattern: "xyznonexistentpatternxyz123",
  59. path: tmp.path,
  60. },
  61. ctx,
  62. ),
  63. )
  64. expect(result.metadata.matches).toBe(0)
  65. expect(result.output).toBe("No files found")
  66. },
  67. })
  68. })
  69. test("handles CRLF line endings in output", async () => {
  70. // This test verifies the regex split handles both \n and \r\n
  71. await using tmp = await tmpdir({
  72. init: async (dir) => {
  73. // Create a test file with content
  74. await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nline3")
  75. },
  76. })
  77. await Instance.provide({
  78. directory: tmp.path,
  79. fn: async () => {
  80. const grep = await initGrep()
  81. const result = await Effect.runPromise(
  82. grep.execute(
  83. {
  84. pattern: "line",
  85. path: tmp.path,
  86. },
  87. ctx,
  88. ),
  89. )
  90. expect(result.metadata.matches).toBeGreaterThan(0)
  91. },
  92. })
  93. })
  94. })
  95. describe("CRLF regex handling", () => {
  96. test("regex correctly splits Unix line endings", () => {
  97. const unixOutput = "file1.txt|1|content1\nfile2.txt|2|content2\nfile3.txt|3|content3"
  98. const lines = unixOutput.trim().split(/\r?\n/)
  99. expect(lines.length).toBe(3)
  100. expect(lines[0]).toBe("file1.txt|1|content1")
  101. expect(lines[2]).toBe("file3.txt|3|content3")
  102. })
  103. test("regex correctly splits Windows CRLF line endings", () => {
  104. const windowsOutput = "file1.txt|1|content1\r\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
  105. const lines = windowsOutput.trim().split(/\r?\n/)
  106. expect(lines.length).toBe(3)
  107. expect(lines[0]).toBe("file1.txt|1|content1")
  108. expect(lines[2]).toBe("file3.txt|3|content3")
  109. })
  110. test("regex handles mixed line endings", () => {
  111. const mixedOutput = "file1.txt|1|content1\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
  112. const lines = mixedOutput.trim().split(/\r?\n/)
  113. expect(lines.length).toBe(3)
  114. })
  115. })