TestChatToolCallFragmentedArgs: `arguments` arrives as JSON fragments, each invalid alone. The client must accumulate raw and parse once at finish_reason.
(t *testing.T)
| 127 | // TestChatToolCallFragmentedArgs: `arguments` arrives as JSON fragments, each |
| 128 | // invalid alone. The client must accumulate raw and parse once at finish_reason. |
| 129 | func TestChatToolCallFragmentedArgs(t *testing.T) { |
| 130 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { |
| 131 | sseOK(w, []string{ |
| 132 | `{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"c1","function":{"name":"bash"}}]}}]}`, |
| 133 | `{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"cmd"}}]}}]}`, |
| 134 | `{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\"ls"}}]}}]}`, |
| 135 | `{"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]}}]}`, |
| 136 | `{"choices":[{"delta":{},"finish_reason":"tool_calls"}],"usage":{"completion_tokens":6}}`, |
| 137 | }) |
| 138 | })) |
| 139 | defer srv.Close() |
| 140 | c := New(srv.URL, "m", "") |
| 141 | var got *chmctx.ToolCall |
| 142 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 143 | if e.Kind == EventToolCall { |
| 144 | got = e.ToolCall |
| 145 | } |
| 146 | } |
| 147 | if got == nil || got.Name != "bash" { |
| 148 | t.Fatalf("tool call missing: %+v", got) |
| 149 | } |
| 150 | if cmd, _ := got.Arguments["cmd"].(string); cmd != "ls" { |
| 151 | t.Fatalf("fragmented args not reassembled - wanted cmd=ls, got %+v", got.Arguments) |
| 152 | } |
| 153 | if len(got.Arguments) != 1 { |
| 154 | t.Fatalf("expected exactly one parsed arg, got %+v", got.Arguments) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | // TestChatToolArgsStreamLive: each tool-call arguments fragment is forwarded as |
| 159 | // an EventToolArgs as it arrives, so the UI can tick its live token estimate |