|
|
@@ -0,0 +1,108 @@
|
|
|
+import { describe, expect, test } from "bun:test"
|
|
|
+import path from "path"
|
|
|
+import { GrepTool } from "../../src/tool/grep"
|
|
|
+import { Instance } from "../../src/project/instance"
|
|
|
+import { tmpdir } from "../fixture/fixture"
|
|
|
+
|
|
|
+const ctx = {
|
|
|
+ sessionID: "test",
|
|
|
+ messageID: "",
|
|
|
+ callID: "",
|
|
|
+ agent: "build",
|
|
|
+ abort: AbortSignal.any([]),
|
|
|
+ metadata: () => {},
|
|
|
+}
|
|
|
+
|
|
|
+const projectRoot = path.join(__dirname, "../..")
|
|
|
+
|
|
|
+describe("tool.grep", () => {
|
|
|
+ test("basic search", async () => {
|
|
|
+ await Instance.provide({
|
|
|
+ directory: projectRoot,
|
|
|
+ fn: async () => {
|
|
|
+ const grep = await GrepTool.init()
|
|
|
+ const result = await grep.execute(
|
|
|
+ {
|
|
|
+ pattern: "export",
|
|
|
+ path: path.join(projectRoot, "src/tool"),
|
|
|
+ include: "*.ts",
|
|
|
+ },
|
|
|
+ ctx,
|
|
|
+ )
|
|
|
+ expect(result.metadata.matches).toBeGreaterThan(0)
|
|
|
+ expect(result.output).toContain("Found")
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ test("no matches returns correct output", async () => {
|
|
|
+ await using tmp = await tmpdir({
|
|
|
+ init: async (dir) => {
|
|
|
+ await Bun.write(path.join(dir, "test.txt"), "hello world")
|
|
|
+ },
|
|
|
+ })
|
|
|
+ await Instance.provide({
|
|
|
+ directory: tmp.path,
|
|
|
+ fn: async () => {
|
|
|
+ const grep = await GrepTool.init()
|
|
|
+ const result = await grep.execute(
|
|
|
+ {
|
|
|
+ pattern: "xyznonexistentpatternxyz123",
|
|
|
+ path: tmp.path,
|
|
|
+ },
|
|
|
+ ctx,
|
|
|
+ )
|
|
|
+ expect(result.metadata.matches).toBe(0)
|
|
|
+ expect(result.output).toBe("No files found")
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ test("handles CRLF line endings in output", async () => {
|
|
|
+ // This test verifies the regex split handles both \n and \r\n
|
|
|
+ await using tmp = await tmpdir({
|
|
|
+ init: async (dir) => {
|
|
|
+ // Create a test file with content
|
|
|
+ await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nline3")
|
|
|
+ },
|
|
|
+ })
|
|
|
+ await Instance.provide({
|
|
|
+ directory: tmp.path,
|
|
|
+ fn: async () => {
|
|
|
+ const grep = await GrepTool.init()
|
|
|
+ const result = await grep.execute(
|
|
|
+ {
|
|
|
+ pattern: "line",
|
|
|
+ path: tmp.path,
|
|
|
+ },
|
|
|
+ ctx,
|
|
|
+ )
|
|
|
+ expect(result.metadata.matches).toBeGreaterThan(0)
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("CRLF regex handling", () => {
|
|
|
+ test("regex correctly splits Unix line endings", () => {
|
|
|
+ const unixOutput = "file1.txt|1|content1\nfile2.txt|2|content2\nfile3.txt|3|content3"
|
|
|
+ const lines = unixOutput.trim().split(/\r?\n/)
|
|
|
+ expect(lines.length).toBe(3)
|
|
|
+ expect(lines[0]).toBe("file1.txt|1|content1")
|
|
|
+ expect(lines[2]).toBe("file3.txt|3|content3")
|
|
|
+ })
|
|
|
+
|
|
|
+ test("regex correctly splits Windows CRLF line endings", () => {
|
|
|
+ const windowsOutput = "file1.txt|1|content1\r\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
|
|
|
+ const lines = windowsOutput.trim().split(/\r?\n/)
|
|
|
+ expect(lines.length).toBe(3)
|
|
|
+ expect(lines[0]).toBe("file1.txt|1|content1")
|
|
|
+ expect(lines[2]).toBe("file3.txt|3|content3")
|
|
|
+ })
|
|
|
+
|
|
|
+ test("regex handles mixed line endings", () => {
|
|
|
+ const mixedOutput = "file1.txt|1|content1\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
|
|
|
+ const lines = mixedOutput.trim().split(/\r?\n/)
|
|
|
+ expect(lines.length).toBe(3)
|
|
|
+ })
|
|
|
+})
|