@@ -0,0 +1,91 @@
1+package provider
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "net/http"
7+ "net/http/httptest"
8+ "os"
9+ "testing"
10+ "time"
11+
12+ "github.com/charmbracelet/crush/internal/config"
13+ "github.com/charmbracelet/crush/internal/fur/provider"
14+ "github.com/charmbracelet/crush/internal/llm/tools"
15+ "github.com/charmbracelet/crush/internal/message"
16+ "github.com/openai/openai-go"
17+ "github.com/openai/openai-go/option"
18+)
19+
20+func TestMain(m *testing.M) {
21+ _, err := config.Init(".", true)
22+ if err != nil {
23+ panic("Failed to initialize config: " + err.Error())
24+ }
25+
26+ os.Exit(m.Run())
27+}
28+
29+func TestOpenAIClientStreamChoices(t *testing.T) {
30+ // Create a mock server that returns Server-Sent Events with empty choices
31+ // This simulates the 🤡 behavior when a server returns 200 instead of 404
32+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+ w.Header().Set("Content-Type", "text/event-stream")
34+ w.Header().Set("Cache-Control", "no-cache")
35+ w.Header().Set("Connection", "keep-alive")
36+ w.WriteHeader(http.StatusOK)
37+
38+ emptyChoicesChunk := map[string]any{
39+ "id": "chat-completion-test",
40+ "object": "chat.completion.chunk",
41+ "created": time.Now().Unix(),
42+ "model": "test-model",
43+ "choices": []any{}, // Empty choices array that causes panic
44+ }
45+
46+ jsonData, _ := json.Marshal(emptyChoicesChunk)
47+ w.Write([]byte("data: " + string(jsonData) + "\n\n"))
48+ w.Write([]byte("data: [DONE]\n\n"))
49+ }))
50+ defer server.Close()
51+
52+ // Create OpenAI client pointing to our mock server
53+ client := &openaiClient{
54+ providerOptions: providerClientOptions{
55+ modelType: config.SelectedModelTypeLarge,
56+ apiKey: "test-key",
57+ systemMessage: "test",
58+ model: func(config.SelectedModelType) provider.Model {
59+ return provider.Model{
60+ ID: "test-model",
61+ Model: "test-model",
62+ }
63+ },
64+ },
65+ client: openai.NewClient(
66+ option.WithAPIKey("test-key"),
67+ option.WithBaseURL(server.URL),
68+ ),
69+ }
70+
71+ // Create test messages
72+ messages := []message.Message{
73+ {
74+ Role: message.User,
75+ Parts: []message.ContentPart{message.TextContent{Text: "Hello"}},
76+ },
77+ }
78+
79+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
80+ defer cancel()
81+
82+ eventsChan := client.stream(ctx, messages, []tools.BaseTool{})
83+
84+ // Collect events - this will panic without the bounds check
85+ for event := range eventsChan {
86+ t.Logf("Received event: %+v", event)
87+ if event.Type == EventError || event.Type == EventComplete {
88+ break
89+ }
90+ }
91+}