summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-04 15:28:38 +0300
committerPaul Buetow <paul@buetow.org>2025-09-04 15:28:38 +0300
commit448d4b169904cfd6e1f701524539a27d8de18734 (patch)
treec102c6d9e228565f54600b3aba173ccdfb75ede6
parent48fac4b473e2564e2e82dad36668277f1071ddd0 (diff)
tests(llm): raise coverage to >=80%\n- Add OpenAI/Copilot HTTP success + stream + token tests\n- Cover With* options and NewFromConfig success paths\n- llm package now ~80.3%
-rw-r--r--internal/llm/copilot_http_test.go37
-rw-r--r--internal/llm/openai_http_test.go22
-rw-r--r--internal/llm/provider_more_test.go26
3 files changed, 85 insertions, 0 deletions
diff --git a/internal/llm/copilot_http_test.go b/internal/llm/copilot_http_test.go
index 2a76b46..c029a65 100644
--- a/internal/llm/copilot_http_test.go
+++ b/internal/llm/copilot_http_test.go
@@ -8,6 +8,8 @@ import (
"net/http/httptest"
"testing"
"time"
+ "strings"
+ "encoding/base64"
)
type rtFunc2 func(*http.Request) (*http.Response, error)
@@ -44,6 +46,41 @@ func TestCopilot_HandleNon2xx(t *testing.T) {
if err := handleCopilotNon2xx(resp, time.Now()); err == nil { t.Fatalf("expected error") }
}
+func TestCopilot_CodeCompletion_Success(t *testing.T) {
+ c := newCopilot("https://api.githubcopilot.com", "gpt-4o-mini", "API", f64p(0.1)).(copilotClient)
+ tr := rtFunc2(func(r *http.Request) (*http.Response, error) {
+ // Token endpoint
+ 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
+ }
+ // Codex completion endpoint
+ if r.URL.Host == "copilot-proxy.githubusercontent.com" && strings.HasSuffix(r.URL.Path, "/v1/engines/copilot-codex/completions") {
+ rw := httptest.NewRecorder()
+ // two choices for index 0 and 1
+ rw.WriteString("data: {\"choices\":[{\"index\":0,\"text\":\"A\"}]}\n")
+ rw.WriteString("data: {\"choices\":[{\"index\":1,\"text\":\"B\"}]}\n")
+ res := rw.Result(); res.StatusCode = 200; return res, nil
+ }
+ return http.DefaultTransport.RoundTrip(r)
+ })
+ c.httpClient = &http.Client{Transport: tr, Timeout: 5 * time.Second}
+ out, err := c.CodeCompletion(context.Background(), "p", "s", 2, "go", 0.1)
+ if err != nil || len(out) != 2 || out[0] != "A" || out[1] != "B" {
+ t.Fatalf("codex: %v %#v", err, out)
+ }
+}
+
+func TestParseJWTExp_AndParseInt64(t *testing.T) {
+ // Valid base64 payload
+ payload := `{"exp": 1700000000}`
+ b := base64.RawURLEncoding.EncodeToString([]byte(payload))
+ tok := "x." + b + ".y"
+ if tm := parseJWTExp(tok); tm.IsZero() { t.Fatalf("expected non-zero time") }
+ if n, err := parseInt64("123"); err != nil || n != 123 { t.Fatalf("parseInt64: %v %d", err, n) }
+}
+
// 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 }
diff --git a/internal/llm/openai_http_test.go b/internal/llm/openai_http_test.go
index 4989067..7ae34be 100644
--- a/internal/llm/openai_http_test.go
+++ b/internal/llm/openai_http_test.go
@@ -3,9 +3,12 @@ package llm
import (
"context"
"encoding/json"
+ "io"
"net/http"
"net/http/httptest"
"testing"
+ "strings"
+ "time"
)
func TestOpenAI_Chat_Success(t *testing.T) {
@@ -25,3 +28,22 @@ func TestOpenAI_Chat_MissingKey(t *testing.T) {
if _, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"hi"}}); err == nil { t.Fatalf("expected error for missing key") }
}
+func TestOpenAI_ChatStream_SSE(t *testing.T) {
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // Return SSE-like stream
+ w.Header().Set("Content-Type", "text/event-stream")
+ io.WriteString(w, "data: {\"choices\":[{\"delta\":{\"content\":\"Hi\"}}]}\n\n")
+ io.WriteString(w, "data: [DONE]\n")
+ }))
+ defer srv.Close()
+ c := newOpenAI(srv.URL, "g", "KEY", f64p(0.2)).(openAIClient)
+ c.httpClient = srv.Client()
+ var got string
+ err := c.ChatStream(context.Background(), []Message{{Role:"user", Content:"hi"}}, func(s string){ got += s })
+ if err != nil || got != "Hi" { t.Fatalf("chat stream: %v %q", err, got) }
+}
+
+func TestHandleOpenAINon2xx_NoErrorBody(t *testing.T) {
+ resp := &http.Response{StatusCode: 500, Body: io.NopCloser(strings.NewReader("{}"))}
+ if err := handleOpenAINon2xx(resp, time.Now()); err == nil { t.Fatalf("expected http error") }
+}
diff --git a/internal/llm/provider_more_test.go b/internal/llm/provider_more_test.go
new file mode 100644
index 0000000..bd08552
--- /dev/null
+++ b/internal/llm/provider_more_test.go
@@ -0,0 +1,26 @@
+package llm
+
+import "testing"
+
+func TestWithOptions_Apply(t *testing.T) {
+ o := Options{}
+ WithModel("m")(&o)
+ WithTemperature(0.7)(&o)
+ WithMaxTokens(123)(&o)
+ WithStop("END")(&o)
+ if o.Model != "m" || o.Temperature != 0.7 || o.MaxTokens != 123 || len(o.Stop) != 1 || o.Stop[0] != "END" {
+ t.Fatalf("options not applied correctly: %+v", o)
+ }
+}
+
+func TestNewFromConfig_Success_OpenAI_And_Copilot(t *testing.T) {
+ // OpenAI success
+ oc := Config{Provider: "openai", OpenAIBaseURL: "http://x", OpenAIModel: "gpt"}
+ c, err := NewFromConfig(oc, "KEY", "")
+ if err != nil || c == nil || c.Name() != "openai" || c.DefaultModel() == "" { t.Fatalf("openai new: %v %v", c, err) }
+ // Copilot success
+ cc := Config{Provider: "copilot", CopilotBaseURL: "http://x", CopilotModel: "gpt-4o-mini"}
+ c2, err := NewFromConfig(cc, "", "KEY")
+ if err != nil || c2 == nil || c2.Name() != "copilot" || c2.DefaultModel() == "" { t.Fatalf("copilot new: %v %v", c2, err) }
+}
+