summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-04 14:35:56 +0300
committerPaul Buetow <paul@buetow.org>2025-09-04 14:35:56 +0300
commit48fac4b473e2564e2e82dad36668277f1071ddd0 (patch)
tree3e4c75e66a32822d35be6ee947a31a61c54261a2
parentd68e5b3b188585fe234d0ce295ec7f054c8bad5f (diff)
tests(llm): add OpenAI and Copilot HTTP tests (success + token/error paths); llm coverage ~61%
-rw-r--r--internal/llm/copilot_http_test.go50
-rw-r--r--internal/llm/openai_http_test.go27
2 files changed, 77 insertions, 0 deletions
diff --git a/internal/llm/copilot_http_test.go b/internal/llm/copilot_http_test.go
new file mode 100644
index 0000000..2a76b46
--- /dev/null
+++ b/internal/llm/copilot_http_test.go
@@ -0,0 +1,50 @@
+package llm
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+)
+
+type rtFunc2 func(*http.Request) (*http.Response, error)
+func (f rtFunc2) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
+
+func TestCopilot_EnsureSession_AndChat_Success(t *testing.T) {
+ // Mock chat endpoint
+ chatSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/chat/completions" { t.Fatalf("unexpected path: %s", r.URL.Path) }
+ _ = json.NewEncoder(w).Encode(map[string]any{"choices": []map[string]any{{"index":0, "message": map[string]string{"role":"assistant","content":"OK"}}}})
+ }))
+ defer chatSrv.Close()
+ c := newCopilot(chatSrv.URL, "gpt-4o-mini", "APIKEY", f64p(0.1)).(copilotClient)
+ // Intercept token endpoint to return a session token
+ tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
+ if r.URL.Host == "api.github.com" && r.URL.Path == "/copilot_internal/v2/token" {
+ rw := httptest.NewRecorder()
+ _ = json.NewEncoder(rw).Encode(map[string]string{"token":"tok"})
+ res := rw.Result()
+ res.StatusCode = 200
+ return res, nil
+ }
+ // Fallback to default transport for chatSrv
+ return http.DefaultTransport.RoundTrip(r)
+ })
+ c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
+ out, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"hi"}})
+ if err != nil || out != "OK" { t.Fatalf("copilot chat failed: %v %q", err, out) }
+}
+
+func TestCopilot_HandleNon2xx(t *testing.T) {
+ b, _ := json.Marshal(map[string]any{"error": map[string]any{"message":"bad","type":"invalid"}})
+ resp := &http.Response{StatusCode: 400, Body: io.NopCloser(bytesReader(b))}
+ if err := handleCopilotNon2xx(resp, time.Now()); err == nil { t.Fatalf("expected error") }
+}
+
+// bytesReader wraps a byte slice with an io.ReadCloser without importing extra.
+type bytesReader []byte
+func (b bytesReader) Read(p []byte) (int, error) { n := copy(p, b); return n, io.EOF }
+func (b bytesReader) Close() error { return nil }
diff --git a/internal/llm/openai_http_test.go b/internal/llm/openai_http_test.go
new file mode 100644
index 0000000..4989067
--- /dev/null
+++ b/internal/llm/openai_http_test.go
@@ -0,0 +1,27 @@
+package llm
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestOpenAI_Chat_Success(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/chat/completions" { t.Fatalf("unexpected path: %s", r.URL.Path) }
+ _ = json.NewEncoder(w).Encode(map[string]any{"choices": []map[string]any{{"index":0, "message": map[string]string{"role":"assistant","content":"OK"}}}})
+ }))
+ defer srv.Close()
+ c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient)
+ c.httpClient = srv.Client()
+ out, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"hi"}})
+ if err != nil || out != "OK" { t.Fatalf("openai chat: %v %q", err, out) }
+}
+
+func TestOpenAI_Chat_MissingKey(t *testing.T) {
+ c := newOpenAI("http://x", "g", "", f64p(0.2)).(openAIClient)
+ if _, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"hi"}}); err == nil { t.Fatalf("expected error for missing key") }
+}
+