@@ -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 ( ) ;
@@ -165,12 +191,134 @@ describe("repetition check accounting at the cycle-text cap", () => {
165191 // detector misses the loop (observed in live thrash fleets).
166192 const window = "I'll open the remaining source files and implement the activity preview. " ;
167193 const zwsp = "\u200B" ;
168- const text = ( window + zwsp ) . repeat ( 12 ) ;
194+ const text = ( window + zwsp ) . repeat ( 20 ) ;
169195 const hit = detectRepetition ( text ) ;
170196 expect ( hit ) . not . toBeNull ( ) ;
171197 expect ( hit ?. repeats ) . toBeGreaterThanOrEqual ( DEFAULT_REPETITION_CONFIG . repeatThreshold ) ;
172198 } ) ;
173199
200+ describe ( "folded text pass (DEFAULT_TEXT_FOLDED_REPETITION_CONFIG)" , ( ) => {
201+ // Mirrors run.ts: digit-preserving default first, capped folded pass second.
202+ function detectText ( text : string ) {
203+ return (
204+ detectRepetition ( text ) ??
205+ detectRepetition ( text , DEFAULT_TEXT_FOLDED_REPETITION_CONFIG , { normalizeDigits : true } )
206+ ) ;
207+ }
208+
209+ test ( "flags an incrementing counter flood in visible text" , ( ) => {
210+ // Observed live: "14279 14280 14281…" streamed to inference-error.
211+ const text = Array . from ( { length : 500 } , ( _ , i ) => `${ 14279 + i } ` ) . join ( "" ) ;
212+ expect ( detectRepetition ( text ) ) . toBeNull ( ) ;
213+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
214+ } ) ;
215+
216+ test ( "flags an incrementing pair-counter flood" , ( ) => {
217+ // Observed live: "5620/5620. 5621/5621. …"
218+ const text = Array . from ( { length : 300 } , ( _ , i ) => `${ 5620 + i } /${ 5620 + i } . ` ) . join ( "" ) ;
219+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
220+ } ) ;
221+
222+ test ( "flags a repeated-timestamp flood" , ( ) => {
223+ // Observed live: "18:22:27." emitted hundreds of times, with drift.
224+ const text = Array . from ( { length : 300 } , ( _ , i ) => `18:22:${ 27 + ( i % 30 ) } . ` ) . join ( "" ) ;
225+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
226+ } ) ;
227+
228+ test ( "flags a zero-percent flood" , ( ) => {
229+ // "0% " is a 3-char unit — under the plain 8-char window floor.
230+ const text = "0% " . repeat ( 200 ) ;
231+ expect ( detectRepetition ( text ) ) . toBeNull ( ) ;
232+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
233+ } ) ;
234+
235+ test ( "flags fence and brace floods" , ( ) => {
236+ expect ( detectText ( "```\n" . repeat ( 100 ) ) ) . not . toBeNull ( ) ;
237+ expect ( detectText ( "}\n" . repeat ( 200 ) ) ) . not . toBeNull ( ) ;
238+ } ) ;
239+
240+ test ( "flags an emoji flood (surrogate-pair unit)" , ( ) => {
241+ // Observed live: "🤔 " ×~40K chars. The unit is 3 UTF-16 units; the
242+ // code-point reversal must keep the pair intact for it to stay periodic.
243+ const text = "🤔 " . repeat ( 2000 ) ;
244+ expect ( detectText ( text ) ) . not . toBeNull ( ) ;
245+ } ) ;
246+
247+ test ( "does not flag a numbered list under the folded text pass" , ( ) => {
248+ // Folds to a ~47-char period — refused by maxFoldedPeriodChars.
249+ const items = Array . from (
250+ { length : 400 } ,
251+ ( _ , i ) => `${ i + 1 } . Ran batch ${ i + 1 } and verified ${ i * 3 } records migrated\n` ,
252+ ) . join ( "" ) ;
253+ expect ( detectText ( `Migration progress:\n${ items } ` ) ) . toBeNull ( ) ;
254+ } ) ;
255+
256+ test ( "does not flag a digit-varying markdown table under the folded text pass" , ( ) => {
257+ const rows = Array . from (
258+ { length : 40 } ,
259+ ( _ , i ) => `| 202${ i % 10 } | ${ i * 10 } requests | ${ i } errors |\n` ,
260+ ) . join ( "" ) ;
261+ expect ( detectText ( `| Year | Volume | Errors |\n|---|---|---|\n${ rows } ` ) ) . toBeNull ( ) ;
262+ } ) ;
263+
264+ test ( "a short user-requested enumeration stays under the folded repeat bar" , ( ) => {
265+ // "print 1..40" folds to "0 " ×40 — under repeatThreshold 64.
266+ const text = Array . from ( { length : 40 } , ( _ , i ) => `${ i + 1 } ` ) . join ( "" ) ;
267+ expect ( detectText ( text ) ) . toBeNull ( ) ;
268+ } ) ;
269+ } ) ;
270+
271+ describe ( "trackContentlessGrowth" , ( ) => {
272+ function feed ( tokens : readonly string [ ] ) : boolean {
273+ let state : ContentlessGrowthState = INITIAL_CONTENTLESS_GROWTH_STATE ;
274+ for ( const token of tokens ) {
275+ const next = trackContentlessGrowth ( state , token ) ;
276+ if ( next . hit ) return true ;
277+ state = next . state ;
278+ }
279+ return false ;
280+ }
281+
282+ test ( "flags a zero-width flood (ZWNJ/ZWJ walls, observed live)" , ( ) => {
283+ // Observed live: 500–53,000 U+200C/U+200D chars per stream.
284+ // detectRepetition strips invisibles before checking, so it must not be
285+ // the only line of defense.
286+ const flood = Array . from ( { length : 60 } , ( ) => "" . repeat ( 32 ) ) ;
287+ expect ( detectRepetition ( flood . join ( "" ) ) ) . toBeNull ( ) ;
288+ expect ( feed ( flood ) ) . toBe ( true ) ;
289+ } ) ;
290+
291+ test ( "flags a flood even when prefixed by healthy prose" , ( ) => {
292+ const tokens = [
293+ "Let me look at the config first. " . repeat ( 4 ) ,
294+ ...Array . from ( { length : 100 } , ( ) => "" . repeat ( 64 ) ) ,
295+ ] ;
296+ expect ( feed ( tokens ) ) . toBe ( true ) ;
297+ } ) ;
298+
299+ test ( "does not flag ordinary prose or sparse code" , ( ) => {
300+ const tokens = Array . from (
301+ { length : 200 } ,
302+ ( _ , i ) => ` const value${ i } = await compute(input${ i } );\n\n` ,
303+ ) ;
304+ expect ( feed ( tokens ) ) . toBe ( false ) ;
305+ } ) ;
306+
307+ test ( "a visible-rich window re-arms rather than latching" , ( ) => {
308+ // Enough visible content inside every window keeps the guard quiet no
309+ // matter how long the stream runs.
310+ const tokens = Array . from ( { length : 50 } , ( ) =>
311+ `${ "" . repeat ( 100 ) } some genuinely visible sentence with plenty of characters. ` ,
312+ ) ;
313+ expect ( feed ( tokens ) ) . toBe ( false ) ;
314+ } ) ;
315+
316+ test ( "whitespace does not count as visible content" , ( ) => {
317+ const raw = " \n\t" . repeat ( DEFAULT_CONTENTLESS_GROWTH_CONFIG . rawWindowChars ) ;
318+ expect ( feed ( [ raw ] ) ) . toBe ( true ) ;
319+ } ) ;
320+ } ) ;
321+
174322describe ( "appendCycleText" , ( ) => {
175323 test ( "keeps only the tail past the cap" , ( ) => {
176324 const text = appendCycleText ( "a" . repeat ( 10 ) , "b" . repeat ( 10 ) , 15 ) ;
0 commit comments