cli.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /// <reference types="bun-types" />
  2. import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
  3. import { mkdir, rm, writeFile } from 'node:fs/promises';
  4. import { tmpdir } from 'node:os';
  5. import { join } from 'node:path';
  6. import { runRg, runRgCount } from './cli';
  7. import { grep } from './tools';
  8. import { formatGrepResult } from './utils';
  9. describe('grep tool', () => {
  10. const testDir = join(tmpdir(), `grep-test-${Date.now()}`);
  11. const testFile1 = join(testDir, 'test1.txt');
  12. const testFile2 = join(testDir, 'test2.ts');
  13. beforeAll(async () => {
  14. await mkdir(testDir, { recursive: true });
  15. await writeFile(
  16. testFile1,
  17. 'Hello world\nThis is a test file\nAnother line with match',
  18. );
  19. await writeFile(
  20. testFile2,
  21. "const x = 'Hello world';\nconsole.log('test');",
  22. );
  23. });
  24. afterAll(async () => {
  25. await rm(testDir, { recursive: true, force: true });
  26. });
  27. describe('formatGrepResult', () => {
  28. test('formats empty results', () => {
  29. const result = {
  30. matches: [],
  31. totalMatches: 0,
  32. filesSearched: 10,
  33. truncated: false,
  34. };
  35. expect(formatGrepResult(result)).toBe('No matches found.');
  36. });
  37. test('formats error results', () => {
  38. const result = {
  39. matches: [],
  40. totalMatches: 0,
  41. filesSearched: 0,
  42. truncated: false,
  43. error: 'Something went wrong',
  44. };
  45. expect(formatGrepResult(result)).toBe('Error: Something went wrong');
  46. });
  47. test('formats matches correctly', () => {
  48. const result = {
  49. matches: [
  50. { file: 'file1.ts', line: 10, text: "const foo = 'bar'" },
  51. { file: 'file1.ts', line: 15, text: 'console.log(foo)' },
  52. { file: 'file2.ts', line: 5, text: "import { foo } from './file1'" },
  53. ],
  54. totalMatches: 3,
  55. filesSearched: 2,
  56. truncated: false,
  57. };
  58. const output = formatGrepResult(result);
  59. expect(output).toContain('file1.ts:');
  60. expect(output).toContain(" 10: const foo = 'bar'");
  61. expect(output).toContain(' 15: console.log(foo)');
  62. expect(output).toContain('file2.ts:');
  63. expect(output).toContain(" 5: import { foo } from './file1'");
  64. expect(output).toContain('Found 3 matches in 2 files');
  65. });
  66. test('indicates truncation', () => {
  67. const result = {
  68. matches: [{ file: 'foo.txt', line: 1, text: 'bar' }],
  69. totalMatches: 100,
  70. filesSearched: 50,
  71. truncated: true,
  72. };
  73. expect(formatGrepResult(result)).toContain('(output truncated)');
  74. });
  75. });
  76. describe('runRg', () => {
  77. test('finds matches in files', async () => {
  78. const result = await runRg({
  79. pattern: 'Hello',
  80. paths: [testDir],
  81. });
  82. expect(result.totalMatches).toBeGreaterThanOrEqual(2);
  83. expect(
  84. result.matches.some(
  85. (m) => m.file.includes('test1.txt') && m.text.includes('Hello'),
  86. ),
  87. ).toBe(true);
  88. expect(
  89. result.matches.some(
  90. (m) => m.file.includes('test2.ts') && m.text.includes('Hello'),
  91. ),
  92. ).toBe(true);
  93. });
  94. test('respects file inclusion patterns', async () => {
  95. const result = await runRg({
  96. pattern: 'Hello',
  97. paths: [testDir],
  98. globs: ['*.txt'],
  99. });
  100. expect(result.matches.some((m) => m.file.includes('test1.txt'))).toBe(
  101. true,
  102. );
  103. expect(result.matches.some((m) => m.file.includes('test2.ts'))).toBe(
  104. false,
  105. );
  106. });
  107. test('handles no matches', async () => {
  108. const result = await runRg({
  109. pattern: 'NonExistentString12345',
  110. paths: [testDir],
  111. });
  112. expect(result.totalMatches).toBe(0);
  113. expect(result.matches).toHaveLength(0);
  114. });
  115. test('respects case sensitivity', async () => {
  116. // Test with exact case match
  117. const resultExact = await runRg({
  118. pattern: 'Hello',
  119. paths: [testDir],
  120. });
  121. expect(resultExact.totalMatches).toBeGreaterThan(0);
  122. // Test with caseSensitive flag set to true (should not match lowercase pattern)
  123. const resultSensitive = await runRg({
  124. pattern: 'hello', // File has "Hello"
  125. paths: [testDir],
  126. caseSensitive: true,
  127. });
  128. expect(resultSensitive.totalMatches).toBe(0);
  129. });
  130. test('respects whole word match', async () => {
  131. const resultPartial = await runRg({
  132. pattern: 'Hell',
  133. paths: [testDir],
  134. wholeWord: false,
  135. });
  136. expect(resultPartial.totalMatches).toBeGreaterThan(0);
  137. const resultWhole = await runRg({
  138. pattern: 'Hell',
  139. paths: [testDir],
  140. wholeWord: true,
  141. });
  142. expect(resultWhole.totalMatches).toBe(0);
  143. });
  144. test('respects max count', async () => {
  145. const result = await runRg({
  146. pattern: 'Hello',
  147. paths: [testDir],
  148. maxCount: 1,
  149. });
  150. // maxCount is per file
  151. expect(
  152. result.matches.filter((m) => m.file.includes('test1.txt')).length,
  153. ).toBeLessThanOrEqual(1);
  154. });
  155. });
  156. describe('runRgCount', () => {
  157. test('counts matches correctly', async () => {
  158. const results = await runRgCount({
  159. pattern: 'Hello',
  160. paths: [testDir],
  161. });
  162. expect(results.length).toBeGreaterThan(0);
  163. const file1Result = results.find((r) => r.file.includes('test1.txt'));
  164. expect(file1Result).toBeDefined();
  165. expect(file1Result?.count).toBe(1);
  166. });
  167. });
  168. describe('grep tool execute', () => {
  169. test('executes successfully', async () => {
  170. // @ts-expect-error
  171. const result = await grep.execute({
  172. pattern: 'Hello',
  173. path: testDir,
  174. });
  175. expect(typeof result).toBe('string');
  176. expect(result).toContain('Found');
  177. expect(result).toContain('matches');
  178. });
  179. test('handles errors gracefully', async () => {
  180. // @ts-expect-error
  181. const result = await grep.execute({
  182. pattern: 'Hello',
  183. path: '/non/existent/path/12345',
  184. });
  185. // Depending on implementation, it might return "No matches found" or an error string
  186. // But it should not throw
  187. expect(typeof result).toBe('string');
  188. });
  189. test('respects include pattern in execute', async () => {
  190. // @ts-expect-error
  191. const result = await grep.execute({
  192. pattern: 'Hello',
  193. path: testDir,
  194. include: '*.txt',
  195. });
  196. expect(result).toContain('test1.txt');
  197. expect(result).not.toContain('test2.ts');
  198. });
  199. });
  200. });