diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-04 08:02:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-04 08:02:58 +0300 |
| commit | 511708f4e892f89fd713e8412f2deea21df8b54a (patch) | |
| tree | 85d83085921ba4320e5edaf7afc252e3194b9ae8 /internal | |
| parent | 7a98d7eeb87c55ae589e78eaf567be29688baffe (diff) | |
llm/ollama: add comprehensive tests (Chat, ChatStream, errors) to reach >80% coverage for ollama.go
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/llm/ollama_test.go | 149 |
1 files changed, 148 insertions, 1 deletions
diff --git a/internal/llm/ollama_test.go b/internal/llm/ollama_test.go index 4ad6fdf..8d77a58 100644 --- a/internal/llm/ollama_test.go +++ b/internal/llm/ollama_test.go @@ -1,6 +1,15 @@ package llm -import "testing" +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) func TestBuildOllamaRequest_OptionsAndStream(t *testing.T) { o := Options{Model: "codemodel", Temperature: 0, MaxTokens: 256, Stop: []string{"STOP"}} @@ -16,3 +25,141 @@ func TestBuildOllamaRequest_OptionsAndStream(t *testing.T) { if !req2.Stream { t.Fatalf("expected stream=true") } } +func TestBuildOllamaRequest_TempOverride(t *testing.T) { + o := Options{Model: "m", Temperature: 0.9} + msgs := []Message{{Role: "user", Content: "hi"}} + req := buildOllamaRequest(o, msgs, f64p(0.2), false) + m := req.Options.(map[string]any) + if m["temperature"].(float64) != 0.9 { t.Fatalf("explicit temp should override default") } +} + +func TestOllama_NameAndModel(t *testing.T) { + c := newOllama("http://x", "model-x", nil).(ollamaClient) + if c.Name() != "ollama" { t.Fatalf("name: %q", c.Name()) } + if c.DefaultModel() != "model-x" { t.Fatalf("default model: %q", c.DefaultModel()) } +} + +func TestOllamaChat_Success(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/chat" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role":"assistant","content":"Hello"}, "done": true}) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", f64p(0.1)).(ollamaClient) + c.httpClient = ts.Client() + out, err := c.Chat(context.Background(), []Message{{Role: "user", Content: "hi"}}) + if err != nil { t.Fatalf("unexpected err: %v", err) } + if out != "Hello" { t.Fatalf("got %q", out) } +} + +func TestOllamaChat_EmptyContent(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]string{"role":"assistant","content":""}, "done": true}) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", nil).(ollamaClient) + c.httpClient = ts.Client() + if _, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"x"}}); err == nil { + t.Fatalf("expected error for empty content") + } +} + +func TestOllamaChat_Non2xx(t *testing.T) { + // API error string + ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(400) + _ = json.NewEncoder(w).Encode(map[string]any{"error":"bad"}) + })) + defer ts1.Close() + c1 := newOllama(ts1.URL, "m", nil).(ollamaClient) + c1.httpClient = ts1.Client() + if _, err := c1.Chat(context.Background(), []Message{{Role:"user", Content:"x"}}); err == nil { + t.Fatalf("expected error for 400 with api body") + } + // Plain HTTP error without api message + ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + _, _ = w.Write([]byte("{}")) + })) + defer ts2.Close() + c2 := newOllama(ts2.URL, "m", nil).(ollamaClient) + c2.httpClient = ts2.Client() + if _, err := c2.Chat(context.Background(), []Message{{Role:"user", Content:"x"}}); err == nil { + t.Fatalf("expected error for 500") + } +} + +type rtFunc func(*http.Request) (*http.Response, error) +func (f rtFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestOllamaChat_HTTPError(t *testing.T) { + c := newOllama("http://127.0.0.1:0", "m", nil).(ollamaClient) + c.httpClient = &http.Client{Transport: rtFunc(func(*http.Request)(*http.Response,error){ return nil, fmt.Errorf("boom") })} + if _, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"x"}}); err == nil { + t.Fatalf("expected http error path") + } +} + +func TestOllamaChat_DecodeError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("{bad json}")) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", nil).(ollamaClient) + c.httpClient = ts.Client() + if _, err := c.Chat(context.Background(), []Message{{Role:"user", Content:"x"}}); err == nil { + t.Fatalf("expected decode error") + } +} + +func TestHandleOllamaNon2xx_OK(t *testing.T) { + resp := &http.Response{StatusCode: 200, Body: ioNopCloser(strings.NewReader(""))} + if err := handleOllamaNon2xx(resp, time.Now()); err != nil { t.Fatalf("unexpected: %v", err) } +} + +func TestOllamaChatStream_Success(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + // two JSON objects back-to-back + _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"Hi"},"done":false}`)) + _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"!"},"done":true}`)) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", nil).(ollamaClient) + c.httpClient = ts.Client() + var got strings.Builder + if err := c.ChatStream(context.Background(), []Message{{Role:"user", Content:"x"}}, func(s string){ got.WriteString(s) }); err != nil { + t.Fatalf("unexpected: %v", err) + } + if got.String() != "Hi!" { t.Fatalf("got %q", got.String()) } +} + +func TestOllamaChatStream_ErrorEvent(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(map[string]any{"error":"oops"}) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", nil).(ollamaClient) + c.httpClient = ts.Client() + if err := c.ChatStream(context.Background(), []Message{{Role:"user", Content:"x"}}, func(string){}); err == nil { + t.Fatalf("expected stream error") + } +} + +func TestOllamaChatStream_DecodeError(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("{not json}")) + })) + defer ts.Close() + c := newOllama(ts.URL, "m", nil).(ollamaClient) + c.httpClient = ts.Client() + if err := c.ChatStream(context.Background(), []Message{{Role:"user", Content:"x"}}, func(string){}); err == nil { + t.Fatalf("expected decode error") + } +} + +// small helper to construct an io.ReadCloser without importing extra packages +type readCloser struct{ *strings.Reader } +func (readCloser) Close() error { return nil } +func ioNopCloser(r *strings.Reader) *readCloser { return &readCloser{r} } |
