@@ -240,6 +240,46 @@ describe("rag: per-file chunking", () => {
240240 expect ( chunks . every ( ( c ) => c . text . length > 0 ) ) . toBe ( true ) ;
241241 } ) ;
242242
243+ it ( "REGRESSION (#7447): does NOT hang when newline snap + near-budget overlap would stall `end - overlap <= start`" , ( ) => {
244+ // Exact repro from the issue: early qualifying newline (~index 60 > chunkChars/2) shrinks `end`, and
245+ // chunkOverlap=99 (allowed by the old clamp of chunkChars-1) makes `end - overlap` land at/before the
246+ // previous start. Without the start+1 floor this never returns.
247+ const text = `${ "x" . repeat ( 60 ) } \n${ "y" . repeat ( 5000 ) } ` ;
248+ const chunks = chunkFile ( "f.py" , text , "" , { chunkChars : 100 , chunkOverlap : 99 } ) ;
249+ expect ( chunks . length ) . toBeGreaterThan ( 1 ) ;
250+ // Worst case every iteration advances exactly 1 char → at most text.length chunks.
251+ expect ( chunks . length ) . toBeLessThanOrEqual ( text . length ) ;
252+ expect ( chunks . every ( ( c ) => c . text . length > 0 ) ) . toBe ( true ) ;
253+ } ) ;
254+
255+ it . each ( [
256+ { chunkChars : 100 , chunkOverlap : 99 } ,
257+ { chunkChars : 100 , chunkOverlap : 50 } ,
258+ { chunkChars : 50 , chunkOverlap : 49 } ,
259+ { chunkChars : 20 , chunkOverlap : 19 } ,
260+ { chunkChars : 1 , chunkOverlap : 0 } ,
261+ { chunkChars : 1 , chunkOverlap : 999 } , // clamped to 0
262+ ] ) (
263+ "REGRESSION (#7447): chunkFile returns finitely for chunkChars=$chunkChars chunkOverlap=$chunkOverlap (forward progress)" ,
264+ ( { chunkChars, chunkOverlap } ) => {
265+ const text = `${ "x" . repeat ( 60 ) } \n${ "y" . repeat ( 5000 ) } ` ;
266+ const chunks = chunkFile ( "f.py" , text , "" , { chunkChars, chunkOverlap } ) ;
267+ expect ( chunks . length ) . toBeGreaterThan ( 0 ) ;
268+ expect ( chunks . length ) . toBeLessThanOrEqual ( text . length ) ;
269+ } ,
270+ ) ;
271+
272+ it ( "REGRESSION (#7447): chunkJsTs oversized-unit fallback through newlineChunks also cannot stall on high overlap" , ( ) => {
273+ // Two logical units so chunkJsTs runs; the first exceeds chunkChars and falls back to newlineChunks with
274+ // the caller-supplied (near-budget) overlap — the other production path that must stay hang-free.
275+ const huge = `export function big() {\n${ "x" . repeat ( 60 ) } \n${ "y" . repeat ( 5000 ) } \n}\n` ;
276+ const small = "export function small() { return 1; }\n" ;
277+ const text = huge + small ;
278+ const chunks = chunkFile ( "src/huge.ts" , text , "" , { chunkChars : 100 , chunkOverlap : 99 } ) ;
279+ expect ( chunks . length ) . toBeGreaterThan ( 0 ) ;
280+ expect ( chunks . length ) . toBeLessThanOrEqual ( text . length ) ;
281+ } ) ;
282+
243283 it ( "PACKS a small multi-function JS file into one chunk (free-tier vector budget unaffected) (#282)" , ( ) => {
244284 const chunks = chunkFile ( "src/small.ts" , "export function a(){return 1;}\nexport function b(){return 2;}\nexport function c(){return 3;}\n" ) ;
245285 expect ( chunks ) . toHaveLength ( 1 ) ;
0 commit comments