@@ -37,6 +37,22 @@ function createRgJsonMatch(
3737 } )
3838}
3939
40+ // Helper to create ripgrep JSON context output (for -A, -B, -C flags)
41+ function createRgJsonContext (
42+ filePath : string ,
43+ lineNumber : number ,
44+ lineText : string ,
45+ ) : string {
46+ return JSON . stringify ( {
47+ type : 'context' ,
48+ data : {
49+ path : { text : filePath } ,
50+ lines : { text : lineText } ,
51+ line_number : lineNumber ,
52+ } ,
53+ } )
54+ }
55+
4056describe ( 'codeSearch' , ( ) => {
4157 let mockSpawn : ReturnType < typeof mock >
4258 let mockProcess : ReturnType < typeof createMockChildProcess >
@@ -87,10 +103,14 @@ describe('codeSearch', () => {
87103 flags : '-A 2' ,
88104 } )
89105
90- // Ripgrep JSON output - only match events are processed
106+ // Ripgrep JSON output with -A 2 includes match + 2 context lines after
91107 const output = [
92108 createRgJsonMatch ( 'test.ts' , 1 , 'import { env } from "./config"' ) ,
109+ createRgJsonContext ( 'test.ts' , 2 , 'const apiUrl = env.API_URL' ) ,
110+ createRgJsonContext ( 'test.ts' , 3 , 'const apiKey = env.API_KEY' ) ,
93111 createRgJsonMatch ( 'other.ts' , 5 , 'import env from "process"' ) ,
112+ createRgJsonContext ( 'other.ts' , 6 , 'const nodeEnv = env.NODE_ENV' ) ,
113+ createRgJsonContext ( 'other.ts' , 7 , 'const port = env.PORT' ) ,
94114 ] . join ( '\n' )
95115
96116 mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
@@ -100,12 +120,15 @@ describe('codeSearch', () => {
100120 expect ( result [ 0 ] . type ) . toBe ( 'json' )
101121 const value = result [ 0 ] . value as any
102122
103- // Should contain both files
104- expect ( value . stdout ) . toContain ( 'test.ts: ' )
105- expect ( value . stdout ) . toContain ( 'other.ts: ' )
123+ // Should contain match lines
124+ expect ( value . stdout ) . toContain ( 'import { env } from "./config" ' )
125+ expect ( value . stdout ) . toContain ( 'import env from "process" ' )
106126
107- // Should not include the entire file content
108- expect ( value . stdout . length ) . toBeLessThan ( 1000 )
127+ // Should contain context lines (this is the bug - they're currently missing)
128+ expect ( value . stdout ) . toContain ( 'const apiUrl = env.API_URL' )
129+ expect ( value . stdout ) . toContain ( 'const apiKey = env.API_KEY' )
130+ expect ( value . stdout ) . toContain ( 'const nodeEnv = env.NODE_ENV' )
131+ expect ( value . stdout ) . toContain ( 'const port = env.PORT' )
109132 } )
110133
111134 it ( 'should correctly parse output with -B flag (before context)' , async ( ) => {
@@ -115,8 +138,13 @@ describe('codeSearch', () => {
115138 flags : '-B 2' ,
116139 } )
117140
141+ // Ripgrep JSON output with -B 2 includes 2 context lines before + match
118142 const output = [
143+ createRgJsonContext ( 'app.ts' , 1 , 'import React from "react"' ) ,
144+ createRgJsonContext ( 'app.ts' , 2 , '' ) ,
119145 createRgJsonMatch ( 'app.ts' , 3 , 'export const main = () => {}' ) ,
146+ createRgJsonContext ( 'utils.ts' , 8 , 'function validateInput(x: string) {' ) ,
147+ createRgJsonContext ( 'utils.ts' , 9 , ' return x.length > 0' ) ,
120148 createRgJsonMatch ( 'utils.ts' , 10 , 'export function helper() {}' ) ,
121149 ] . join ( '\n' )
122150
@@ -126,8 +154,14 @@ describe('codeSearch', () => {
126154 const result = await searchPromise
127155 const value = result [ 0 ] . value as any
128156
129- expect ( value . stdout ) . toContain ( 'app.ts:' )
130- expect ( value . stdout ) . toContain ( 'utils.ts:' )
157+ // Should contain match lines
158+ expect ( value . stdout ) . toContain ( 'export const main = () => {}' )
159+ expect ( value . stdout ) . toContain ( 'export function helper() {}' )
160+
161+ // Should contain before context lines
162+ expect ( value . stdout ) . toContain ( 'import React from "react"' )
163+ expect ( value . stdout ) . toContain ( 'function validateInput(x: string) {' )
164+ expect ( value . stdout ) . toContain ( 'return x.length > 0' )
131165 } )
132166
133167 it ( 'should correctly parse output with -C flag (context before and after)' , async ( ) => {
@@ -137,16 +171,73 @@ describe('codeSearch', () => {
137171 flags : '-C 1' ,
138172 } )
139173
140- const output = createRgJsonMatch ( 'code.ts' , 6 , ' // TODO: implement this' )
174+ // Ripgrep JSON output with -C 1 includes 1 line before + match + 1 line after
175+ const output = [
176+ createRgJsonContext ( 'code.ts' , 5 , 'function processData() {' ) ,
177+ createRgJsonMatch ( 'code.ts' , 6 , ' // TODO: implement this' ) ,
178+ createRgJsonContext ( 'code.ts' , 7 , ' return null' ) ,
179+ ] . join ( '\n' )
180+
181+ mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
182+ mockProcess . emit ( 'close' , 0 )
183+
184+ const result = await searchPromise
185+ const value = result [ 0 ] . value as any
186+
187+ // Should contain match line
188+ expect ( value . stdout ) . toContain ( 'TODO: implement this' )
189+
190+ // Should contain context lines before and after
191+ expect ( value . stdout ) . toContain ( 'function processData() {' )
192+ expect ( value . stdout ) . toContain ( 'return null' )
193+ } )
194+
195+ it ( 'should handle -A flag with multiple matches in same file' , async ( ) => {
196+ const searchPromise = codeSearch ( {
197+ projectPath : '/test/project' ,
198+ pattern : 'import' ,
199+ flags : '-A 1' ,
200+ } )
201+
202+ const output = [
203+ createRgJsonMatch ( 'file.ts' , 1 , 'import foo from "foo"' ) ,
204+ createRgJsonContext ( 'file.ts' , 2 , 'import bar from "bar"' ) ,
205+ createRgJsonMatch ( 'file.ts' , 3 , 'import baz from "baz"' ) ,
206+ createRgJsonContext ( 'file.ts' , 4 , '' ) ,
207+ ] . join ( '\n' )
208+
209+ mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
210+ mockProcess . emit ( 'close' , 0 )
211+
212+ const result = await searchPromise
213+ const value = result [ 0 ] . value as any
214+
215+ // Should contain all matches
216+ expect ( value . stdout ) . toContain ( 'import foo from "foo"' )
217+ expect ( value . stdout ) . toContain ( 'import baz from "baz"' )
218+
219+ // Context line appears as both context and match
220+ expect ( value . stdout ) . toContain ( 'import bar from "bar"' )
221+ } )
222+
223+ it ( 'should handle -B flag at start of file' , async ( ) => {
224+ const searchPromise = codeSearch ( {
225+ projectPath : '/test/project' ,
226+ pattern : 'import' ,
227+ flags : '-B 2' ,
228+ } )
229+
230+ // First line match has no before context
231+ const output = createRgJsonMatch ( 'file.ts' , 1 , 'import foo from "foo"' )
141232
142233 mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
143234 mockProcess . emit ( 'close' , 0 )
144235
145236 const result = await searchPromise
146237 const value = result [ 0 ] . value as any
147238
148- expect ( value . stdout ) . toContain ( 'code.ts:' )
149- expect ( value . stdout ) . toContain ( 'TODO ' )
239+ // Should still work with match at file start
240+ expect ( value . stdout ) . toContain ( 'import foo from "foo" ' )
150241 } )
151242
152243 it ( 'should skip separator lines between result groups' , async ( ) => {
@@ -261,9 +352,13 @@ describe('codeSearch', () => {
261352
262353 const output = [
263354 createRgJsonMatch ( 'file.ts' , 1 , 'test 1' ) ,
355+ createRgJsonContext ( 'file.ts' , 2 , 'context 1' ) ,
264356 createRgJsonMatch ( 'file.ts' , 5 , 'test 2' ) ,
357+ createRgJsonContext ( 'file.ts' , 6 , 'context 2' ) ,
265358 createRgJsonMatch ( 'file.ts' , 10 , 'test 3' ) ,
359+ createRgJsonContext ( 'file.ts' , 11 , 'context 3' ) ,
266360 createRgJsonMatch ( 'file.ts' , 15 , 'test 4' ) ,
361+ createRgJsonContext ( 'file.ts' , 16 , 'context 4' ) ,
267362 ] . join ( '\n' )
268363
269364 mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
@@ -272,11 +367,19 @@ describe('codeSearch', () => {
272367 const result = await searchPromise
273368 const value = result [ 0 ] . value as any
274369
275- // Should be limited to 2 results per file
370+ // Should be limited to 2 match results per file (context lines don't count toward limit)
276371 // Count how many 'test' matches are in the output
277372 const testMatches = ( value . stdout . match ( / t e s t \d / g) || [ ] ) . length
278373 expect ( testMatches ) . toBeLessThanOrEqual ( 2 )
279374 expect ( value . stdout ) . toContain ( 'Results limited' )
375+
376+ // Should still include context lines for the matches that are shown
377+ if ( value . stdout . includes ( 'test 1' ) ) {
378+ expect ( value . stdout ) . toContain ( 'context 1' )
379+ }
380+ if ( value . stdout . includes ( 'test 2' ) ) {
381+ expect ( value . stdout ) . toContain ( 'context 2' )
382+ }
280383 } )
281384
282385 it ( 'should respect globalMaxResults with context lines' , async ( ) => {
@@ -289,9 +392,13 @@ describe('codeSearch', () => {
289392
290393 const output = [
291394 createRgJsonMatch ( 'file1.ts' , 1 , 'test 1' ) ,
395+ createRgJsonContext ( 'file1.ts' , 2 , 'context 1' ) ,
292396 createRgJsonMatch ( 'file1.ts' , 5 , 'test 2' ) ,
397+ createRgJsonContext ( 'file1.ts' , 6 , 'context 2' ) ,
293398 createRgJsonMatch ( 'file2.ts' , 1 , 'test 3' ) ,
399+ createRgJsonContext ( 'file2.ts' , 2 , 'context 3' ) ,
294400 createRgJsonMatch ( 'file2.ts' , 5 , 'test 4' ) ,
401+ createRgJsonContext ( 'file2.ts' , 6 , 'context 4' ) ,
295402 ] . join ( '\n' )
296403
297404 mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
@@ -300,7 +407,7 @@ describe('codeSearch', () => {
300407 const result = await searchPromise
301408 const value = result [ 0 ] . value as any
302409
303- // Should be limited globally to 3 results
410+ // Should be limited globally to 3 match results (context lines don't count)
304411 const matches = ( value . stdout . match ( / t e s t \d / g) || [ ] ) . length
305412 expect ( matches ) . toBeLessThanOrEqual ( 3 )
306413 // Check for either 'Global limit' message or truncation indicator
@@ -309,6 +416,38 @@ describe('codeSearch', () => {
309416 value . stdout . includes ( 'Results limited' )
310417 expect ( hasLimitMessage ) . toBe ( true )
311418 } )
419+
420+ it ( 'should not count context lines toward maxResults limit' , async ( ) => {
421+ const searchPromise = codeSearch ( {
422+ projectPath : '/test/project' ,
423+ pattern : 'match' ,
424+ flags : '-A 2 -B 2' ,
425+ maxResults : 1 ,
426+ } )
427+
428+ const output = [
429+ createRgJsonContext ( 'file.ts' , 1 , 'context before 1' ) ,
430+ createRgJsonContext ( 'file.ts' , 2 , 'context before 2' ) ,
431+ createRgJsonMatch ( 'file.ts' , 3 , 'match line' ) ,
432+ createRgJsonContext ( 'file.ts' , 4 , 'context after 1' ) ,
433+ createRgJsonContext ( 'file.ts' , 5 , 'context after 2' ) ,
434+ ] . join ( '\n' )
435+
436+ mockProcess . stdout . emit ( 'data' , Buffer . from ( output ) )
437+ mockProcess . emit ( 'close' , 0 )
438+
439+ const result = await searchPromise
440+ const value = result [ 0 ] . value as any
441+
442+ // Should include the match
443+ expect ( value . stdout ) . toContain ( 'match line' )
444+
445+ // Should include all context lines even though maxResults is 1
446+ expect ( value . stdout ) . toContain ( 'context before 1' )
447+ expect ( value . stdout ) . toContain ( 'context before 2' )
448+ expect ( value . stdout ) . toContain ( 'context after 1' )
449+ expect ( value . stdout ) . toContain ( 'context after 2' )
450+ } )
312451 } )
313452
314453 describe ( 'malformed output handling' , ( ) => {
0 commit comments