go-rocm/internal/llamacpp/client_test.go
Claude 1bc8c9948b
test: completion streaming tests for llamacpp client
Add TestComplete_Streaming (multi-chunk SSE with three tokens) and
TestComplete_HTTPError (400 status propagation) to exercise the
Complete() method alongside the existing chat tests.

Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 20:59:21 +00:00

194 lines
5.1 KiB
Go

package llamacpp
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// sseLines writes SSE-formatted lines to a flushing response writer.
func sseLines(w http.ResponseWriter, lines []string) {
f, ok := w.(http.Flusher)
if !ok {
panic("ResponseWriter does not implement Flusher")
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
for _, line := range lines {
fmt.Fprintf(w, "data: %s\n\n", line)
f.Flush()
}
}
func TestChatComplete_Streaming(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
assert.Equal(t, "POST", r.Method)
sseLines(w, []string{
`{"choices":[{"delta":{"content":"Hello"},"finish_reason":null}]}`,
`{"choices":[{"delta":{"content":" world"},"finish_reason":null}]}`,
`{"choices":[{"delta":{},"finish_reason":"stop"}]}`,
"[DONE]",
})
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.ChatComplete(context.Background(), ChatRequest{
Messages: []ChatMessage{{Role: "user", Content: "Hi"}},
MaxTokens: 64,
Temperature: 0.0,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
}
require.NoError(t, errFn())
assert.Equal(t, []string{"Hello", " world"}, got)
}
func TestChatComplete_EmptyResponse(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sseLines(w, []string{"[DONE]"})
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.ChatComplete(context.Background(), ChatRequest{
Messages: []ChatMessage{{Role: "user", Content: "Hi"}},
Temperature: 0.7,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
}
require.NoError(t, errFn())
assert.Empty(t, got)
}
func TestChatComplete_HTTPError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError)
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.ChatComplete(context.Background(), ChatRequest{
Messages: []ChatMessage{{Role: "user", Content: "Hi"}},
Temperature: 0.7,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
}
assert.Empty(t, got)
err := errFn()
require.Error(t, err)
assert.Contains(t, err.Error(), "500")
}
func TestChatComplete_ContextCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
f, ok := w.(http.Flusher)
if !ok {
panic("ResponseWriter does not implement Flusher")
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
// Send first chunk.
fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"Hello"},"finish_reason":null}]}`)
f.Flush()
// Wait for context cancellation before sending more.
<-r.Context().Done()
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.ChatComplete(ctx, ChatRequest{
Messages: []ChatMessage{{Role: "user", Content: "Hi"}},
Temperature: 0.7,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
cancel() // Cancel after receiving the first token.
}
// The error may or may not be nil depending on timing;
// the important thing is we got exactly 1 token.
_ = errFn()
assert.Equal(t, []string{"Hello"}, got)
}
func TestComplete_Streaming(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/v1/completions", r.URL.Path)
assert.Equal(t, "POST", r.Method)
sseLines(w, []string{
`{"choices":[{"text":"Once","finish_reason":null}]}`,
`{"choices":[{"text":" upon","finish_reason":null}]}`,
`{"choices":[{"text":" a time","finish_reason":null}]}`,
`{"choices":[{"text":"","finish_reason":"stop"}]}`,
"[DONE]",
})
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.Complete(context.Background(), CompletionRequest{
Prompt: "Once",
MaxTokens: 64,
Temperature: 0.0,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
}
require.NoError(t, errFn())
assert.Equal(t, []string{"Once", " upon", " a time"}, got)
}
func TestComplete_HTTPError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad request", http.StatusBadRequest)
}))
defer ts.Close()
c := NewClient(ts.URL)
tokens, errFn := c.Complete(context.Background(), CompletionRequest{
Prompt: "Hello",
Temperature: 0.7,
Stream: true,
})
var got []string
for tok := range tokens {
got = append(got, tok)
}
assert.Empty(t, got)
err := errFn()
require.Error(t, err)
assert.Contains(t, err.Error(), "400")
}