Skip to content

Commit 7252d8a

Browse files
authored
Merge pull request #239 from shelltime/claude/improve-query-error-messages-yoro8
Extract error messages from AI service error responses
2 parents 3143fa2 + 99ad1dd commit 7252d8a

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

model/ai_service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"encoding/json"
88
"fmt"
9+
"io"
910
"net/http"
1011
"strings"
1112
"time"
@@ -56,6 +57,13 @@ func (s *sseAIService) QueryCommandStream(
5657
defer resp.Body.Close()
5758

5859
if resp.StatusCode != http.StatusOK {
60+
body, readErr := io.ReadAll(resp.Body)
61+
if readErr == nil {
62+
var errResp errorResponse
63+
if jsonErr := json.Unmarshal(body, &errResp); jsonErr == nil && errResp.ErrorMessage != "" {
64+
return fmt.Errorf("%s", errResp.ErrorMessage)
65+
}
66+
}
5967
return fmt.Errorf("server returned status %d", resp.StatusCode)
6068
}
6169

model/ai_service_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package model
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
)
11+
12+
func TestQueryCommandStream_ErrorResponseBody(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
statusCode int
16+
responseBody interface{}
17+
expectedErrMsg string
18+
}{
19+
{
20+
name: "quota exceeded returns error message from body",
21+
statusCode: http.StatusTooManyRequests,
22+
responseBody: errorResponse{
23+
ErrorCode: http.StatusTooManyRequests,
24+
ErrorMessage: "monthly AI credit quota exceeded",
25+
},
26+
expectedErrMsg: "monthly AI credit quota exceeded",
27+
},
28+
{
29+
name: "unauthorized returns error message from body",
30+
statusCode: http.StatusUnauthorized,
31+
responseBody: errorResponse{
32+
ErrorCode: http.StatusUnauthorized,
33+
ErrorMessage: "unauthorized",
34+
},
35+
expectedErrMsg: "unauthorized",
36+
},
37+
{
38+
name: "service unavailable returns error message from body",
39+
statusCode: http.StatusServiceUnavailable,
40+
responseBody: errorResponse{
41+
ErrorCode: http.StatusServiceUnavailable,
42+
ErrorMessage: "AI service is not available",
43+
},
44+
expectedErrMsg: "AI service is not available",
45+
},
46+
{
47+
name: "non-JSON response falls back to status code",
48+
statusCode: http.StatusInternalServerError,
49+
responseBody: "not json",
50+
expectedErrMsg: fmt.Sprintf("server returned status %d", http.StatusInternalServerError),
51+
},
52+
{
53+
name: "empty error message falls back to status code",
54+
statusCode: http.StatusBadRequest,
55+
responseBody: errorResponse{
56+
ErrorCode: http.StatusBadRequest,
57+
ErrorMessage: "",
58+
},
59+
expectedErrMsg: fmt.Sprintf("server returned status %d", http.StatusBadRequest),
60+
},
61+
}
62+
63+
for _, tt := range tests {
64+
t.Run(tt.name, func(t *testing.T) {
65+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66+
w.Header().Set("Content-Type", "application/json")
67+
w.WriteHeader(tt.statusCode)
68+
if s, ok := tt.responseBody.(string); ok {
69+
w.Write([]byte(s))
70+
} else {
71+
json.NewEncoder(w).Encode(tt.responseBody)
72+
}
73+
}))
74+
defer server.Close()
75+
76+
svc := NewAIService()
77+
err := svc.QueryCommandStream(
78+
context.Background(),
79+
CommandSuggestVariables{Shell: "bash", Os: "linux", Query: "test"},
80+
Endpoint{APIEndpoint: server.URL, Token: "test-token"},
81+
func(token string) {},
82+
)
83+
84+
if err == nil {
85+
t.Fatal("expected error, got nil")
86+
}
87+
if err.Error() != tt.expectedErrMsg {
88+
t.Errorf("expected error %q, got %q", tt.expectedErrMsg, err.Error())
89+
}
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)