Skip to content

Commit 732a588

Browse files
committed
add multi-tool E2E test for WebUI pipeline
TestServe_E2E_MultiToolCall verifies the full WebUI event sequence with 2 tool_call → tool_result cycles within a single prompt turn. Exercises the same code path the WebUI uses for sub-agent delegation rendering. Verifies: session, token, tool_call, tool_result, done events in order with correct count.
1 parent 5c0f9b0 commit 732a588

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

cmd/kode/serve_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)