@@ -151,6 +151,92 @@ func TestRunProgressiveSessionFinalizesOnNaturalCompletion(t *testing.T) {
151151 }
152152}
153153
154+ func TestRunProgressiveSessionTimeoutDoesNotStartDetachedFinalizationAfterDeadline (t * testing.T ) {
155+ provider := llm .NewMockProvider ("mock" )
156+ provider .AddTurn (llm.MockTurn {Delay : 50 * time .Millisecond , Text : "too slow" })
157+ provider .AddTextResponse ("finalization should not run" )
158+
159+ engine := llm .NewEngine (provider , nil )
160+ ctx , cancel := context .WithTimeout (context .Background (), 20 * time .Millisecond )
161+ defer cancel ()
162+
163+ result , err := runProgressiveSession (ctx , engine , llm.Request {
164+ Messages : []llm.Message {llm .UserText ("Investigate X" )},
165+ MaxTurns : 2 ,
166+ }, progressiveRunOptions {StopWhen : progressiveStopWhenTimeout })
167+ if err != nil {
168+ t .Fatalf ("runProgressiveSession error = %v" , err )
169+ }
170+ if result .ExitReason != exitReasonTimeout {
171+ t .Fatalf ("exit reason = %q, want %q" , result .ExitReason , exitReasonTimeout )
172+ }
173+ if len (provider .Requests ) != 1 {
174+ t .Fatalf ("provider saw %d requests, want timeout to skip detached finalization pass" , len (provider .Requests ))
175+ }
176+ }
177+
178+ func TestProgressiveFinalizationContextNaturalCompletionDetachesFromParent (t * testing.T ) {
179+ parent , cancelParent := context .WithCancel (context .Background ())
180+ finalizeCtx , finalizeCancel := progressiveFinalizationContext (parent , time .Second , exitReasonNatural )
181+ if finalizeCtx == nil {
182+ t .Fatal ("expected finalization context" )
183+ }
184+ defer finalizeCancel ()
185+
186+ deadline , ok := finalizeCtx .Deadline ()
187+ if ! ok {
188+ t .Fatal ("expected deadline on finalization context" )
189+ }
190+ if remaining := time .Until (deadline ); remaining < progressiveDefaultFinalizeGrace - time .Second {
191+ t .Fatalf ("natural finalization remaining = %v, want about %v" , remaining , progressiveDefaultFinalizeGrace )
192+ }
193+
194+ cancelParent ()
195+ select {
196+ case <- finalizeCtx .Done ():
197+ t .Fatal ("natural finalization context should not be canceled with parent" )
198+ default :
199+ }
200+ }
201+
202+ func TestProgressiveFinalizationContextTimeoutUsesReserveAndParentCancellation (t * testing.T ) {
203+ parent , cancelParent := context .WithCancel (context .Background ())
204+ defer cancelParent ()
205+
206+ reserve := 50 * time .Millisecond
207+ finalizeCtx , finalizeCancel := progressiveFinalizationContext (parent , reserve , exitReasonTimeout )
208+ if finalizeCtx == nil {
209+ t .Fatal ("expected finalization context" )
210+ }
211+ defer finalizeCancel ()
212+
213+ deadline , ok := finalizeCtx .Deadline ()
214+ if ! ok {
215+ t .Fatal ("expected deadline on finalization context" )
216+ }
217+ remaining := time .Until (deadline )
218+ if remaining <= 0 || remaining > time .Second {
219+ t .Fatalf ("timeout finalization remaining = %v, want reserve-sized budget without 5m grace" , remaining )
220+ }
221+
222+ cancelParent ()
223+ select {
224+ case <- finalizeCtx .Done ():
225+ case <- time .After (250 * time .Millisecond ):
226+ t .Fatal ("timeout finalization context did not stop when parent was canceled" )
227+ }
228+ }
229+
230+ func TestProgressiveFinalizationContextCancelledExitSkipsWhenParentAlreadyCancelled (t * testing.T ) {
231+ parent , cancelParent := context .WithCancel (context .Background ())
232+ cancelParent ()
233+
234+ finalizeCtx , finalizeCancel := progressiveFinalizationContext (parent , time .Second , exitReasonCancelled )
235+ if finalizeCtx != nil || finalizeCancel != nil {
236+ t .Fatal ("expected cancelled exit to skip finalization when parent is already canceled" )
237+ }
238+ }
239+
154240func TestRunProgressivePassDoesNotDuplicateProducedAssistantWhenResponseCallbackFails (t * testing.T ) {
155241 provider := llm .NewMockProvider ("mock" ).WithCapabilities (llm.Capabilities {ToolCalls : true })
156242 provider .AddToolCall ("call-1" , "progressive_test_tool" , map [string ]any {})
0 commit comments