@@ -3,9 +3,14 @@ import { describe, expect, test } from "bun:test";
33import { appendCycleText , CYCLE_TEXT_CAP_CHARS } from "../session/stream-journal.js" ;
44import {
55 detectRepetition ,
6+ DEFAULT_CONTENTLESS_GROWTH_CONFIG ,
67 DEFAULT_REPETITION_CONFIG ,
8+ DEFAULT_TEXT_FOLDED_REPETITION_CONFIG ,
79 DEFAULT_THINKING_REPETITION_CONFIG ,
10+ INITIAL_CONTENTLESS_GROWTH_STATE ,
811 REPETITION_CHECK_INTERVAL_CHARS ,
12+ trackContentlessGrowth ,
13+ type ContentlessGrowthState ,
914} from "./repetition.js" ;
1015
1116// A monotonic counter that never repeats verbatim: each pair's numerator and
@@ -21,8 +26,9 @@ const LOOP_SENTENCE =
2126describe ( "detectRepetition" , ( ) => {
2227 test ( "flags a looped status sentence with an oscillating counter" , ( ) => {
2328 // Counters that flip between values keep the raw text periodic — the
24- // period just spans one full oscillation (two sentences here).
25- const iterations = Array . from ( { length : 20 } , ( _ , i ) =>
29+ // period just spans one full oscillation (two sentences here), so hitting
30+ // the repeat threshold takes twice as many iterations.
31+ const iterations = Array . from ( { length : 40 } , ( _ , i ) =>
2632 LOOP_SENTENCE . replace ( "0/1.0" , `${ i % 2 } /1.0` ) ,
2733 ) ;
2834 const text = `some earlier legitimate prose about the task. ${ iterations . join ( "" ) } ` ;
@@ -73,6 +79,26 @@ describe("detectRepetition", () => {
7379 expect ( detectRepetition ( looped ) ) . not . toBeNull ( ) ;
7480 } ) ;
7581
82+ test ( "flags a short-phrase loop (10-char unit, observed live)" , ( ) => {
83+ // The second captured incident: "Groaning. " emitted ~1,363 times. The
84+ // old 16-char window floor never saw a 10-char unit.
85+ const text = "Groaning. " . repeat ( 1300 ) ;
86+ const hit = detectRepetition ( text ) ;
87+ expect ( hit ) . not . toBeNull ( ) ;
88+ expect ( hit ?. window ) . toBe ( "Groaning. " ) ;
89+ expect ( hit ?. repeats ) . toBeGreaterThanOrEqual ( DEFAULT_REPETITION_CONFIG . repeatThreshold ) ;
90+ } ) ;
91+
92+ test ( "does not flag a repeated markdown table separator row" , ( ) => {
93+ const row = "| ---------------------- | ---------------------- |\n" ;
94+ expect ( detectRepetition ( `| Left | Right |\n${ row . repeat ( 6 ) } ` ) ) . toBeNull ( ) ;
95+ } ) ;
96+
97+ test ( "does not flag a few identical code lines" , ( ) => {
98+ const line = " const result = await fetchData(request, options, context)\n" ;
99+ expect ( detectRepetition ( line . repeat ( 3 ) ) ) . toBeNull ( ) ;
100+ } ) ;
101+
76102 test ( "returns null for text shorter than one full window set" , ( ) => {
77103 expect ( detectRepetition ( "short" ) ) . toBeNull ( ) ;
78104 expect ( detectRepetition ( "" ) ) . toBeNull ( ) ;
@@ -170,12 +196,135 @@ test("flags a loop that injects zero-width spaces between identical windows", ()
170196 // detector misses the loop (observed in live thrash fleets).
171197 const window = "I'll open the remaining source files and implement the activity preview. " ;
172198 const zwsp = "\u200B" ;
173- const text = ( window + zwsp ) . repeat ( 12 ) ;
199+ const text = ( window + zwsp ) . repeat ( 20 ) ;
174200 const hit = detectRepetition ( text ) ;
175201 expect ( hit ) . not . toBeNull ( ) ;
176202 expect ( hit ?. repeats ) . toBeGreaterThanOrEqual ( DEFAULT_REPETITION_CONFIG . repeatThreshold ) ;
177203} ) ;
178204
205+ describe ( "folded text pass (DEFAULT_TEXT_FOLDED_REPETITION_CONFIG)" , ( ) => {
206+ // Mirrors run.ts: digit-preserving default first, capped folded pass second.
207+ function detectText ( text : string ) {
208+ return (
209+ detectRepetition ( text ) ??
210+ detectRepetition ( text , DEFAULT_TEXT_FOLDED_REPETITION_CONFIG , { normalizeDigits : true } )
211+ ) ;
212+ }
213+
214+ test ( "flags an incrementing counter flood in visible text" , ( ) => {
215+ // Observed live: "14279 14280 14281…" streamed to inference-error.
216+ const text = Array . from ( { length : 500 } , ( _ , i ) => `${ 14279 + i } ` ) . join ( "" ) ;
217+ expect ( detectRepetition ( text ) ) . toBeNull ( ) ;
218+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
219+ } ) ;
220+
221+ test ( "flags an incrementing pair-counter flood" , ( ) => {
222+ // Observed live: "5620/5620. 5621/5621. …"
223+ const text = Array . from ( { length : 300 } , ( _ , i ) => `${ 5620 + i } /${ 5620 + i } . ` ) . join ( "" ) ;
224+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
225+ } ) ;
226+
227+ test ( "flags a repeated-timestamp flood" , ( ) => {
228+ // Observed live: "18:22:27." emitted hundreds of times, with drift.
229+ const text = Array . from ( { length : 300 } , ( _ , i ) => `18:22:${ 27 + ( i % 30 ) } . ` ) . join ( "" ) ;
230+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
231+ } ) ;
232+
233+ test ( "flags a zero-percent flood" , ( ) => {
234+ // "0% " is a 3-char unit — under the plain 8-char window floor.
235+ const text = "0% " . repeat ( 200 ) ;
236+ expect ( detectRepetition ( text ) ) . toBeNull ( ) ;
237+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
238+ } ) ;
239+
240+ test ( "flags fence and brace floods" , ( ) => {
241+ expect ( detectText ( "```\n" . repeat ( 100 ) ) ) . not . toBeNull ( ) ;
242+ expect ( detectText ( "}\n" . repeat ( 200 ) ) ) . not . toBeNull ( ) ;
243+ } ) ;
244+
245+ test ( "flags an emoji flood (surrogate-pair unit)" , ( ) => {
246+ // Observed live: "🤔 " ×~40K chars. The unit is 3 UTF-16 units; the
247+ // code-point reversal must keep the pair intact for it to stay periodic.
248+ const text = "🤔 " . repeat ( 2000 ) ;
249+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
250+ } ) ;
251+
252+ test ( "does not flag a numbered list under the folded text pass" , ( ) => {
253+ // Folds to a ~47-char period — refused by maxFoldedPeriodChars.
254+ const items = Array . from (
255+ { length : 400 } ,
256+ ( _ , i ) => `${ i + 1 } . Ran batch ${ i + 1 } and verified ${ i * 3 } records migrated\n` ,
257+ ) . join ( "" ) ;
258+ expect ( detectText ( `Migration progress:\n${ items } ` ) ) . toBeNull ( ) ;
259+ } ) ;
260+
261+ test ( "does not flag a digit-varying markdown table under the folded text pass" , ( ) => {
262+ const rows = Array . from (
263+ { length : 40 } ,
264+ ( _ , i ) => `| 202${ i % 10 } | ${ i * 10 } requests | ${ i } errors |\n` ,
265+ ) . join ( "" ) ;
266+ expect ( detectText ( `| Year | Volume | Errors |\n|---|---|---|\n${ rows } ` ) ) . toBeNull ( ) ;
267+ } ) ;
268+
269+ test ( "a short user-requested enumeration stays under the folded repeat bar" , ( ) => {
270+ // "print 1..40" folds to "0 " ×40 — under repeatThreshold 64.
271+ const text = Array . from ( { length : 40 } , ( _ , i ) => `${ i + 1 } ` ) . join ( "" ) ;
272+ expect ( detectText ( text ) ) . toBeNull ( ) ;
273+ } ) ;
274+ } ) ;
275+
276+ describe ( "trackContentlessGrowth" , ( ) => {
277+ function feed ( tokens : readonly string [ ] ) : boolean {
278+ let state : ContentlessGrowthState = INITIAL_CONTENTLESS_GROWTH_STATE ;
279+ for ( const token of tokens ) {
280+ const next = trackContentlessGrowth ( state , token ) ;
281+ if ( next . hit ) return true ;
282+ state = next . state ;
283+ }
284+ return false ;
285+ }
286+
287+ test ( "flags a zero-width flood (ZWNJ/ZWJ walls, observed live)" , ( ) => {
288+ // Observed live: 500–53,000 U+200C/U+200D chars per stream.
289+ // detectRepetition strips invisibles before checking, so it must not be
290+ // the only line of defense.
291+ const flood = Array . from ( { length : 60 } , ( ) => "" . repeat ( 32 ) ) ;
292+ expect ( detectRepetition ( flood . join ( "" ) ) ) . toBeNull ( ) ;
293+ expect ( feed ( flood ) ) . toBe ( true ) ;
294+ } ) ;
295+
296+ test ( "flags a flood even when prefixed by healthy prose" , ( ) => {
297+ const tokens = [
298+ "Let me look at the config first. " . repeat ( 4 ) ,
299+ ...Array . from ( { length : 100 } , ( ) => "" . repeat ( 64 ) ) ,
300+ ] ;
301+ expect ( feed ( tokens ) ) . toBe ( true ) ;
302+ } ) ;
303+
304+ test ( "does not flag ordinary prose or sparse code" , ( ) => {
305+ const tokens = Array . from (
306+ { length : 200 } ,
307+ ( _ , i ) => ` const value${ i } = await compute(input${ i } );\n\n` ,
308+ ) ;
309+ expect ( feed ( tokens ) ) . toBe ( false ) ;
310+ } ) ;
311+
312+ test ( "a visible-rich window re-arms rather than latching" , ( ) => {
313+ // Enough visible content inside every window keeps the guard quiet no
314+ // matter how long the stream runs.
315+ const tokens = Array . from (
316+ { length : 50 } ,
317+ ( ) => `${ "" . repeat ( 100 ) } some genuinely visible sentence with plenty of characters. ` ,
318+ ) ;
319+ expect ( feed ( tokens ) ) . toBe ( false ) ;
320+ } ) ;
321+
322+ test ( "whitespace does not count as visible content" , ( ) => {
323+ const raw = " \n\t" . repeat ( DEFAULT_CONTENTLESS_GROWTH_CONFIG . rawWindowChars ) ;
324+ expect ( feed ( [ raw ] ) ) . toBe ( true ) ;
325+ } ) ;
326+ } ) ;
327+
179328describe ( "appendCycleText" , ( ) => {
180329 test ( "keeps only the tail past the cap" , ( ) => {
181330 const text = appendCycleText ( "a" . repeat ( 10 ) , "b" . repeat ( 10 ) , 15 ) ;
0 commit comments