@@ -2057,6 +2057,95 @@ ASSERT:
20572057 }
20582058}
20592059
2060+ func TestDispatchRequestFrameRunMaxTurnFailurePublishesStopReason (t * testing.T ) {
2061+ relay := NewStreamRelay (StreamRelayOptions {})
2062+ ctx , cancel := context .WithCancel (context .Background ())
2063+ defer cancel ()
2064+
2065+ connectionID := NewConnectionID ()
2066+ connectionCtx := WithConnectionID (ctx , connectionID )
2067+ connectionCtx = WithStreamRelay (connectionCtx , relay )
2068+
2069+ messageCh := make (chan RelayMessage , 8 )
2070+ if err := relay .RegisterConnection (ConnectionRegistration {
2071+ ConnectionID : connectionID ,
2072+ Channel : StreamChannelIPC ,
2073+ Context : connectionCtx ,
2074+ Cancel : cancel ,
2075+ Write : func (message RelayMessage ) error {
2076+ messageCh <- message
2077+ return nil
2078+ },
2079+ Close : func () {},
2080+ }); err != nil {
2081+ t .Fatalf ("register connection: %v" , err )
2082+ }
2083+ defer relay .dropConnection (connectionID )
2084+
2085+ if err := relay .BindConnection (connectionID , StreamBinding {
2086+ SessionID : "run-session-max-turn" ,
2087+ RunID : "run-max-turn" ,
2088+ Channel : StreamChannelIPC ,
2089+ Role : StreamRoleNone ,
2090+ Explicit : true ,
2091+ }); err != nil {
2092+ t .Fatalf ("bind connection: %v" , err )
2093+ }
2094+
2095+ runtime := & bootstrapRuntimeStub {
2096+ runFn : func (_ context.Context , _ RunInput ) error {
2097+ return NewRuntimeMaxTurnExceededError ("runtime: max turn limit reached (40)" )
2098+ },
2099+ }
2100+ response := dispatchRequestFrame (connectionCtx , MessageFrame {
2101+ Type : FrameTypeRequest ,
2102+ Action : FrameActionRun ,
2103+ RequestID : "req-run-max-turn" ,
2104+ SessionID : "run-session-max-turn" ,
2105+ RunID : "run-max-turn" ,
2106+ InputText : "hello" ,
2107+ }, runtime )
2108+ if response .Type != FrameTypeAck {
2109+ t .Fatalf ("response type = %q, want %q" , response .Type , FrameTypeAck )
2110+ }
2111+
2112+ deadline := time .After (2 * time .Second )
2113+ for {
2114+ select {
2115+ case message := <- messageCh :
2116+ notification , ok := message .Payload .(protocol.JSONRPCNotification )
2117+ if ! ok || notification .Method != protocol .MethodGatewayEvent {
2118+ continue
2119+ }
2120+ eventFrame := MessageFrame {}
2121+ raw , err := json .Marshal (notification .Params )
2122+ if err != nil {
2123+ t .Fatalf ("marshal payload params: %v" , err )
2124+ }
2125+ if err := json .Unmarshal (raw , & eventFrame ); err != nil {
2126+ t .Fatalf ("unmarshal event frame: %v" , err )
2127+ }
2128+ payloadMap , _ := eventFrame .Payload .(map [string ]any )
2129+ if strings .TrimSpace (fmt .Sprint (payloadMap ["event_type" ])) != string (RuntimeEventTypeRunError ) {
2130+ continue
2131+ }
2132+ envelope , _ := payloadMap ["payload" ].(map [string ]any )
2133+ if got := strings .TrimSpace (fmt .Sprint (envelope ["code" ])); got != ErrorCodeMaxTurnExceeded .String () {
2134+ t .Fatalf ("payload.code = %q, want %q" , got , ErrorCodeMaxTurnExceeded .String ())
2135+ }
2136+ if got := strings .TrimSpace (fmt .Sprint (envelope ["stop_reason" ])); got != ErrorCodeMaxTurnExceeded .String () {
2137+ t .Fatalf ("payload.stop_reason = %q, want %q" , got , ErrorCodeMaxTurnExceeded .String ())
2138+ }
2139+ if got := strings .TrimSpace (fmt .Sprint (envelope ["message" ])); got != "runtime: max turn limit reached (40)" {
2140+ t .Fatalf ("payload.message = %q, want max turn detail" , got )
2141+ }
2142+ return
2143+ case <- deadline :
2144+ t .Fatal ("expected max-turn run_error event" )
2145+ }
2146+ }
2147+ }
2148+
20602149func TestRuntimeCallFailedFrameSanitizesErrorAndMapsCode (t * testing.T ) {
20612150 var buf bytes.Buffer
20622151 ctx := WithGatewayLogger (context .Background (), log .New (& buf , "" , 0 ))
@@ -2108,6 +2197,19 @@ func TestRuntimeCallFailedFrameSanitizesErrorAndMapsCode(t *testing.T) {
21082197 if invalidActionErr .Error .Message != "approve_plan invalid action" {
21092198 t .Fatalf ("invalid action message = %q, want %q" , invalidActionErr .Error .Message , "approve_plan invalid action" )
21102199 }
2200+
2201+ maxTurnErr := runtimeCallFailedFrame (
2202+ context .Background (),
2203+ frame ,
2204+ NewRuntimeMaxTurnExceededError ("runtime: max turn limit reached (40)" ),
2205+ "run" ,
2206+ )
2207+ if maxTurnErr .Error == nil || maxTurnErr .Error .Code != ErrorCodeMaxTurnExceeded .String () {
2208+ t .Fatalf ("max turn error payload = %#v, want max_turn_exceeded" , maxTurnErr .Error )
2209+ }
2210+ if maxTurnErr .Error .Message != "runtime: max turn limit reached (40)" {
2211+ t .Fatalf ("max turn message = %q, want runtime detail" , maxTurnErr .Error .Message )
2212+ }
21112213}
21122214
21132215func TestNormalizeRunID (t * testing.T ) {
0 commit comments