diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-04 15:28:38 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-04 15:28:38 +0300 |
| commit | 448d4b169904cfd6e1f701524539a27d8de18734 (patch) | |
| tree | c102c6d9e228565f54600b3aba173ccdfb75ede6 | |
| parent | 48fac4b473e2564e2e82dad36668277f1071ddd0 (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.go | 37 | ||||
| -rw-r--r-- | internal/llm/openai_http_test.go | 22 | ||||
| -rw-r--r-- | internal/llm/provider_more_test.go | 26 |
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) } +} + |
