@@ -175,6 +175,80 @@ describe("createPerfReactorObserver", () => {
175175 obs . observe ( inferenceDone ( ) ) ;
176176 expect ( byName ( completed ( snapshot ( ) ) , "turn" ) ) . toHaveLength ( 2 ) ;
177177 } ) ;
178+
179+ test ( "abandon mid-inference then new start closes prior turn with no orphans" , ( ) => {
180+ const obs = createPerfReactorObserver ( ) ;
181+
182+ obs . observe ( event ( "inference.start" , { model : "test-model" } ) ) ;
183+ obs . observe ( event ( "inference.text.delta" , { token : "partial" , partial : { text : "partial" } } ) ) ;
184+ // Interrupt: no inference.done / error — next start must abandon.
185+ obs . observe ( event ( "inference.start" , { model : "test-model" } ) ) ;
186+ obs . observe ( inferenceDone ( ) ) ;
187+
188+ const spans = snapshot ( ) ;
189+ expect ( spans . every ( ( s ) => s . endNs !== undefined ) ) . toBe ( true ) ;
190+
191+ const turns = byName ( completed ( spans ) , "turn" ) ;
192+ const inferences = byName ( completed ( spans ) , "inference" ) ;
193+ expect ( turns ) . toHaveLength ( 2 ) ;
194+ expect ( inferences ) . toHaveLength ( 2 ) ;
195+ expect ( inferences [ 0 ] ! . parentId ) . toBe ( turns [ 0 ] ! . id ) ;
196+ expect ( inferences [ 1 ] ! . parentId ) . toBe ( turns [ 1 ] ! . id ) ;
197+ // First turn abandoned before second opened — not nested.
198+ expect ( turns [ 0 ] ! . endNs ! <= turns [ 1 ] ! . startNs ) . toBe ( true ) ;
199+ } ) ;
200+
201+ test ( "abandon mid-tool then new start closes open tools and prior turn" , ( ) => {
202+ const obs = createPerfReactorObserver ( ) ;
203+
204+ obs . observe ( event ( "inference.start" , { model : "test-model" } ) ) ;
205+ obs . observe (
206+ inferenceDone ( [
207+ { type : "tool_call" , id : "call-1" , name : "run_shell" , arguments : { } } ,
208+ ] ) ,
209+ ) ;
210+ obs . observe ( event ( "tool.start" , { call : { id : "call-1" , name : "run_shell" , arguments : { } } } ) ) ;
211+ // Interrupt mid-tool: no tool.done — next inference.start must not nest.
212+ obs . observe ( event ( "inference.start" , { model : "next-model" } ) ) ;
213+ obs . observe ( inferenceDone ( ) ) ;
214+
215+ const spans = snapshot ( ) ;
216+ expect ( spans . every ( ( s ) => s . endNs !== undefined ) ) . toBe ( true ) ;
217+
218+ const turns = byName ( completed ( spans ) , "turn" ) ;
219+ const tools = byName ( completed ( spans ) , "tool" ) ;
220+ const inferences = byName ( completed ( spans ) , "inference" ) ;
221+
222+ expect ( turns ) . toHaveLength ( 2 ) ;
223+ expect ( tools ) . toHaveLength ( 1 ) ;
224+ expect ( tools [ 0 ] ! . parentId ) . toBe ( turns [ 0 ] ! . id ) ;
225+ expect ( inferences ) . toHaveLength ( 2 ) ;
226+ expect ( inferences [ 0 ] ! . parentId ) . toBe ( turns [ 0 ] ! . id ) ;
227+ expect ( inferences [ 1 ] ! . parentId ) . toBe ( turns [ 1 ] ! . id ) ;
228+ // Second inference must not nest under the abandoned turn.
229+ expect ( inferences [ 1 ] ! . parentId ) . not . toBe ( turns [ 0 ] ! . id ) ;
230+ expect ( turns [ 0 ] ! . endNs ! <= turns [ 1 ] ! . startNs ) . toBe ( true ) ;
231+ } ) ;
232+
233+ test ( "inference.error mid-turn then new start leaves no open spans" , ( ) => {
234+ const obs = createPerfReactorObserver ( ) ;
235+
236+ obs . observe ( event ( "inference.start" , { model : "test-model" } ) ) ;
237+ obs . observe ( event ( "inference.text.delta" , { token : "x" , partial : { text : "x" } } ) ) ;
238+ obs . observe ( event ( "inference.error" , { error : { message : "timeout" } } ) ) ;
239+
240+ // Error with no pending tools closes the turn.
241+ expect ( snapshot ( ) . every ( ( s ) => s . endNs !== undefined ) ) . toBe ( true ) ;
242+
243+ obs . observe ( event ( "inference.start" , { model : "retry-model" } ) ) ;
244+ obs . observe ( inferenceDone ( ) ) ;
245+
246+ const spans = snapshot ( ) ;
247+ expect ( spans . every ( ( s ) => s . endNs !== undefined ) ) . toBe ( true ) ;
248+ const turns = byName ( completed ( spans ) , "turn" ) ;
249+ expect ( turns ) . toHaveLength ( 2 ) ;
250+ expect ( byName ( completed ( spans ) , "inference" ) [ 1 ] ! . parentId ) . toBe ( turns [ 1 ] ! . id ) ;
251+ } ) ;
178252} ) ;
179253
180254describe ( "turn collector durationMs unchanged with perf observer" , ( ) => {
0 commit comments