@@ -91,4 +91,93 @@ describe("SandboxProcessStream async iterator", () => {
9191
9292 expect ( lines ) . toEqual ( [ "only-line\n" ] ) ;
9393 } ) ;
94+
95+ test ( "recovers new data when server buffer rotates (drops old data from front)" , async ( ) => {
96+ // Simulate a server with a fixed-size stdout buffer of 15 chars.
97+ // Once output exceeds that, old data is dropped from the front.
98+ const BUFFER_SIZE = 15 ;
99+ const allLines = [
100+ "AAAA\n" , // cumulative: 5 chars
101+ "BBBB\n" , // cumulative: 10 chars
102+ "CCCC\n" , // cumulative: 15 chars — buffer full
103+ "DDDD\n" , // cumulative: 20 chars — "AAAA\n" dropped from front
104+ "EEEE\n" , // cumulative: 25 chars — "BBBB\n" dropped from front
105+ ] ;
106+
107+ // Build the sequence of server responses, trimming the front at BUFFER_SIZE
108+ let fullOutput = "" ;
109+ const serverResponses : string [ ] = [ ] ;
110+ for ( const line of allLines ) {
111+ fullOutput += line ;
112+ if ( fullOutput . length > BUFFER_SIZE ) {
113+ fullOutput = fullOutput . slice ( fullOutput . length - BUFFER_SIZE ) ;
114+ }
115+ serverResponses . push ( fullOutput ) ;
116+ }
117+ // serverResponses:
118+ // [0] "AAAA\n" (5 chars)
119+ // [1] "AAAA\nBBBB\n" (10 chars)
120+ // [2] "AAAA\nBBBB\nCCCC\n" (15 chars) — buffer full
121+ // [3] "BBBB\nCCCC\nDDDD\n" (15 chars) — "AAAA\n" dropped
122+ // [4] "CCCC\nDDDD\nEEEE\n" (15 chars) — "BBBB\n" dropped
123+
124+ let fetchIndex = 0 ;
125+ const stream = new SandboxProcessStream (
126+ makeMockProcess ( 0 ) ,
127+ ( ) => {
128+ if ( fetchIndex < serverResponses . length ) {
129+ return serverResponses [ fetchIndex ++ ] ;
130+ }
131+ return serverResponses [ serverResponses . length - 1 ] ;
132+ }
133+ ) ;
134+
135+ const lines : string [ ] = [ ] ;
136+ for await ( const line of stream ) {
137+ lines . push ( line ) ;
138+ }
139+
140+ expect ( lines ) . toEqual ( [
141+ "AAAA\n" ,
142+ "BBBB\n" ,
143+ "CCCC\n" ,
144+ "DDDD\n" ,
145+ "EEEE\n" ,
146+ ] ) ;
147+ } ) ;
148+
149+ test ( "returns entire output when rotation leaves no overlap with previous buffer" , async ( ) => {
150+ // Simulate a server where the buffer is completely replaced between
151+ // two consecutive fetches — zero characters in common.
152+ const serverResponses = [
153+ "AAAA\nBBBB\n" , // fetch 0: initial data
154+ "CCCC\nDDDD\n" , // fetch 1: completely different content (no overlap)
155+ ] ;
156+
157+ let fetchIndex = 0 ;
158+ const stream = new SandboxProcessStream (
159+ makeMockProcess ( 0 ) ,
160+ ( ) => {
161+ if ( fetchIndex < serverResponses . length ) {
162+ return serverResponses [ fetchIndex ++ ] ;
163+ }
164+ return serverResponses [ serverResponses . length - 1 ] ;
165+ }
166+ ) ;
167+
168+ const lines : string [ ] = [ ] ;
169+ for await ( const line of stream ) {
170+ lines . push ( line ) ;
171+ }
172+
173+ // With no overlap the fallback returns the entire new buffer,
174+ // which may duplicate data already yielded — but duplicates are
175+ // preferable to silent data loss.
176+ expect ( lines ) . toEqual ( [
177+ "AAAA\n" ,
178+ "BBBB\n" ,
179+ "CCCC\n" ,
180+ "DDDD\n" ,
181+ ] ) ;
182+ } ) ;
94183} ) ;
0 commit comments