@@ -313,8 +313,8 @@ describe("contextValidator", () => {
313313 // With the new implementation, when content exceeds limit even after cutback,
314314 // it returns MIN_USEFUL_LINES (50) as the minimum
315315 expect ( result . safeMaxLines ) . toBe ( 50 )
316- expect ( result . reason ) . toContain ( "File exceeds available context space" )
317- expect ( result . reason ) . toContain ( "Safely read 50 lines" )
316+ expect ( result . reason ) . toContain ( "Very limited context space" )
317+ expect ( result . reason ) . toContain ( "Limited to 50 lines" )
318318 } )
319319
320320 it ( "should handle negative available space gracefully" , async ( ) => {
@@ -353,8 +353,8 @@ describe("contextValidator", () => {
353353 expect ( result . shouldLimit ) . toBe ( true )
354354 // When available space is negative, it returns MIN_USEFUL_LINES (50)
355355 expect ( result . safeMaxLines ) . toBe ( 50 ) // MIN_USEFUL_LINES from the refactored code
356- expect ( result . reason ) . toContain ( "File exceeds available context space" )
357- expect ( result . reason ) . toContain ( "Safely read 50 lines" )
356+ expect ( result . reason ) . toContain ( "Very limited context space" )
357+ expect ( result . reason ) . toContain ( "Limited to 50 lines" )
358358 } )
359359
360360 it ( "should limit file when it is too large and would be truncated" , async ( ) => {
@@ -418,7 +418,7 @@ describe("contextValidator", () => {
418418 expect ( result . shouldLimit ) . toBe ( true )
419419 // With the new implementation, when space is very limited and content exceeds,
420420 // it returns the minimal safe value
421- expect ( result . reason ) . toContain ( "File exceeds available context space" )
421+ expect ( result . reason ) . toContain ( "Very limited context space" )
422422 } )
423423
424424 it ( "should not limit when file fits within context" , async ( ) => {
@@ -677,14 +677,133 @@ describe("contextValidator", () => {
677677
678678 // Should limit the file
679679 expect ( result . shouldLimit ) . toBe ( true )
680- expect ( result . safeMaxLines ) . toBe ( 0 )
681- expect ( result . reason ) . toContain ( "Minified file exceeds available context space" )
682- expect ( result . reason ) . toContain ( "80000 tokens" )
683- expect ( result . reason ) . toContain ( "Consider using search_files" )
680+ expect ( result . safeMaxLines ) . toBe ( 1 ) // Single-line files return 1 when truncated
681+ expect ( result . reason ) . toContain ( "Large single-line file" )
682+ expect ( result . reason ) . toContain ( "Only the first" )
684683
685684 // Should have attempted to read and count tokens
686685 expect ( readLines ) . toHaveBeenCalledWith ( filePath , 0 , 0 )
687- expect ( mockTask . api . countTokens ) . toHaveBeenCalledWith ( [ { type : "text" , text : hugeMinifiedContent } ] )
686+ expect ( mockTask . api . countTokens ) . toHaveBeenCalled ( )
687+ } )
688+
689+ it ( "should apply char/3 heuristic and 20% backoff for large single-line files" , async ( ) => {
690+ const filePath = "/test/large-minified.js"
691+ const totalLines = 1
692+ const currentMaxReadFileLine = - 1
693+
694+ // Mock a large single-line file
695+ vi . mocked ( fs . stat ) . mockResolvedValue ( {
696+ size : 2 * 1024 * 1024 , // 2MB
697+ } as any )
698+
699+ // Create a very large single line that exceeds estimated safe chars
700+ const largeContent = "x" . repeat ( 300000 ) // 300K chars
701+ vi . mocked ( readLines ) . mockResolvedValue ( largeContent )
702+
703+ // Mock token counting to always exceed limit, forcing maximum cutbacks
704+ mockTask . api . countTokens = vi . fn ( ) . mockResolvedValue ( 100000 ) // Always exceeds ~57k limit
705+
706+ const result = await validateFileSizeForContext ( filePath , totalLines , currentMaxReadFileLine , mockTask )
707+
708+ // After maximum cutbacks, it should still limit the file
709+ expect ( result . shouldLimit ) . toBe ( true )
710+
711+ // Check that it either returns safeMaxLines: 1 (truncated) or 0 (can't fit any)
712+ expect ( [ 0 , 1 ] ) . toContain ( result . safeMaxLines )
713+
714+ if ( result . safeMaxLines === 1 ) {
715+ expect ( result . reason ) . toContain ( "Large single-line file" )
716+ expect ( result . reason ) . toContain ( "Only the first" )
717+ expect ( result . reason ) . toContain ( "This is a hard limit" )
718+ } else {
719+ expect ( result . reason ) . toContain ( "Single-line file is too large" )
720+ expect ( result . reason ) . toContain ( "This file cannot be accessed" )
721+ }
722+
723+ // Should have made multiple API calls due to cutbacks
724+ expect ( mockTask . api . countTokens ) . toHaveBeenCalledTimes ( 5 ) // MAX_API_CALLS
725+ } )
726+
727+ it ( "should handle single-line files that fit after cutback" , async ( ) => {
728+ const filePath = "/test/borderline-minified.js"
729+ const totalLines = 1
730+ const currentMaxReadFileLine = - 1
731+
732+ // Mock file size
733+ vi . mocked ( fs . stat ) . mockResolvedValue ( {
734+ size : 800 * 1024 , // 800KB
735+ } as any )
736+
737+ // Create content that's just over the limit
738+ const content = "const x=1;" . repeat ( 20000 ) // ~200KB
739+ vi . mocked ( readLines ) . mockResolvedValue ( content )
740+
741+ // Mock token counting - first call exceeds, second fits
742+ let callCount = 0
743+ mockTask . api . countTokens = vi . fn ( ) . mockImplementation ( async ( content ) => {
744+ callCount ++
745+ const text = content [ 0 ] . text
746+ if ( callCount === 1 ) {
747+ return 65000 // Just over the ~57k limit
748+ }
749+ // After 20% cutback
750+ return 45000 // Now fits comfortably
751+ } )
752+
753+ const result = await validateFileSizeForContext ( filePath , totalLines , currentMaxReadFileLine , mockTask )
754+
755+ // Should limit but allow partial read
756+ expect ( result . shouldLimit ) . toBe ( true )
757+ expect ( result . safeMaxLines ) . toBe ( 1 )
758+ expect ( result . reason ) . toContain ( "Large single-line file" )
759+
760+ // Verify percentage calculation in reason
761+ if ( result . reason ) {
762+ const match = result . reason . match ( / O n l y t h e f i r s t ( \d + ) % / )
763+ expect ( match ) . toBeTruthy ( )
764+ if ( match ) {
765+ const percentage = parseInt ( match [ 1 ] )
766+ expect ( percentage ) . toBeGreaterThan ( 0 )
767+ expect ( percentage ) . toBeLessThan ( 100 )
768+ }
769+ }
770+
771+ // Should have made 2 API calls (initial + after cutback)
772+ expect ( mockTask . api . countTokens ) . toHaveBeenCalledTimes ( 2 )
773+ } )
774+
775+ it ( "should handle single-line files that cannot fit any content" , async ( ) => {
776+ const filePath = "/test/impossible-minified.js"
777+ const totalLines = 1
778+ const currentMaxReadFileLine = - 1
779+
780+ // Mock file size
781+ vi . mocked ( fs . stat ) . mockResolvedValue ( {
782+ size : 10 * 1024 * 1024 , // 10MB
783+ } as any )
784+
785+ // Mock very high context usage
786+ mockTask . getTokenUsage = vi . fn ( ) . mockReturnValue ( {
787+ contextTokens : 99000 , // 99% used
788+ } )
789+
790+ // Create massive content
791+ const content = "x" . repeat ( 1000000 )
792+ vi . mocked ( readLines ) . mockResolvedValue ( content )
793+
794+ // Mock token counting - always exceeds even after cutbacks
795+ mockTask . api . countTokens = vi . fn ( ) . mockResolvedValue ( 100000 )
796+
797+ const result = await validateFileSizeForContext ( filePath , totalLines , currentMaxReadFileLine , mockTask )
798+
799+ // Should completely block the file
800+ expect ( result . shouldLimit ) . toBe ( true )
801+ expect ( result . safeMaxLines ) . toBe ( 0 )
802+ expect ( result . reason ) . toContain ( "Single-line file is too large to read any portion" )
803+ expect ( result . reason ) . toContain ( "This file cannot be accessed" )
804+
805+ // Should have tried multiple times
806+ expect ( mockTask . api . countTokens ) . toHaveBeenCalled ( )
688807 } )
689808
690809 it ( "should fall back to regular validation if single-line processing fails" , async ( ) => {
0 commit comments