@@ -829,3 +829,118 @@ func waitForHTTP(t *testing.T, addr string) {
829829 t .Fatal ("server not ready after 5s" )
830830}
831831
832+ // TestServe_E2E_MultiToolCall verifies the WebUI event sequence when
833+ // the agent makes multiple tool calls in a single turn. This exercises
834+ // the same code path the WebUI uses for sub-agent delegation rendering.
835+ func TestServe_E2E_MultiToolCall (t * testing.T ) {
836+ // Mock LLM: make two shell tool calls, then a final response.
837+ callCount := 0
838+ llmSrv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
839+ callCount ++
840+ w .Header ().Set ("Content-Type" , "application/json" )
841+ if callCount <= 2 {
842+ fmt .Fprintf (w , `{"choices":[{"message":{"content":"Running step %d.","tool_calls":[{"id":"call_%d","function":{"name":"shell","arguments":"{\"command\":\"echo step %d\"}"}}]}}]}` ,
843+ callCount , callCount , callCount )
844+ } else {
845+ w .Write ([]byte (`{"choices":[{"message":{"content":"All done."}}]}` ))
846+ }
847+ }))
848+ defer llmSrv .Close ()
849+
850+ envCleanup := setTestEnv (t , llmSrv .URL )
851+ defer envCleanup ()
852+
853+ store := newTestSessionStore (t )
854+ ln , mux := buildServeMux (t , store )
855+ defer ln .Close ()
856+
857+ errCh := make (chan error , 1 )
858+ go func () { errCh <- serveOnListener (ln , mux ) }()
859+ waitForHTTP (t , ln .Addr ().String ())
860+
861+ wsURL := "ws://" + ln .Addr ().String () + "/ws"
862+ conn , err := golangws .Dial (wsURL , "" , "http://localhost" )
863+ if err != nil {
864+ t .Fatalf ("Dial(%q): %v" , wsURL , err )
865+ }
866+ defer conn .Close ()
867+ t .Log ("✅ WebSocket connected" )
868+
869+ // Send a prompt
870+ prompt := map [string ]string {"type" : "prompt" , "content" : "run multiple steps" }
871+ payload , _ := json .Marshal (prompt )
872+ if err := golangws .Message .Send (conn , string (payload )); err != nil {
873+ t .Fatalf ("Send: %v" , err )
874+ }
875+ t .Log ("✅ Prompt sent" )
876+
877+ // Collect all events
878+ conn .SetReadDeadline (time .Now ().Add (15 * time .Second ))
879+
880+ var events []map [string ]any
881+ var sawSession , sawToken , sawToolCall , sawToolResult , sawDone bool
882+ var toolCallCount , toolResultCount int
883+
884+ for i := 0 ; i < 15 ; i ++ {
885+ var raw []byte
886+ if err := golangws .Message .Receive (conn , & raw ); err != nil {
887+ t .Fatalf ("Receive event %d: %v (collected %d events)" , i , err , len (events ))
888+ }
889+ t .Logf (" event[%d]: %s" , i , string (raw ))
890+
891+ var evt map [string ]any
892+ if err := json .Unmarshal (raw , & evt ); err != nil {
893+ t .Fatalf ("unmarshal event %d: %v" , i , err )
894+ }
895+ events = append (events , evt )
896+
897+ switch evt ["type" ] {
898+ case "session" :
899+ sawSession = true
900+ case "token" :
901+ sawToken = true
902+ case "tool_call" :
903+ sawToolCall = true
904+ toolCallCount ++
905+ case "tool_result" :
906+ sawToolResult = true
907+ toolResultCount ++
908+ case "done" :
909+ sawDone = true
910+ goto multiDone
911+ case "error" :
912+ t .Fatalf ("unexpected error: %v" , evt ["message" ])
913+ }
914+ }
915+
916+ multiDone:
917+ if ! sawSession {
918+ t .Fatal ("E2E: missing session event" )
919+ }
920+ if ! sawToken {
921+ t .Fatal ("E2E: missing token event" )
922+ }
923+ if ! sawToolCall {
924+ t .Fatal ("E2E: missing tool_call event" )
925+ }
926+ if ! sawToolResult {
927+ t .Fatal ("E2E: missing tool_result event" )
928+ }
929+ if toolCallCount < 2 {
930+ t .Errorf ("E2E: expected at least 2 tool_calls, got %d" , toolCallCount )
931+ }
932+ if toolResultCount < 2 {
933+ t .Errorf ("E2E: expected at least 2 tool_results, got %d" , toolResultCount )
934+ }
935+ if ! sawDone {
936+ t .Fatal ("E2E: missing done event" )
937+ }
938+
939+ types := make ([]string , len (events ))
940+ for i , e := range events {
941+ types [i ] = e ["type" ].(string )
942+ }
943+ t .Logf ("✅ Multi-tool E2E event sequence: %v" , types )
944+ t .Log ("✅ Multi-tool pipeline verified: session → tokens → tool_call → tool_result → (repeat) → done" )
945+ }
946+
0 commit comments