Files
mistral-go-sdk/stream_test.go
vikingowl 4686ed6898 feat: Phase 2 streaming — SSE parser, Stream[T], ChatCompleteStream
Add streaming infrastructure:
- SSE line parser handling multi-line data, comments, [DONE] sentinel
- Generic Stream[T] pull-based iterator (no goroutines, no channel leaks)
- doStream() HTTP helper for streaming endpoints
- ChatCompleteStream() method
- 28 new tests: SSE edge cases, iterator behavior, httptest integration
2026-03-05 19:33:07 +01:00

142 lines
3.1 KiB
Go

package mistral
import (
"io"
"strings"
"testing"
)
type testChunk struct {
ID string `json:"id"`
Content string `json:"content"`
}
func newTestStream(sse string) *Stream[testChunk] {
body := io.NopCloser(strings.NewReader(sse))
return newStream[testChunk](body)
}
func TestStream_SingleChunk(t *testing.T) {
input := "data: {\"id\":\"1\",\"content\":\"hello\"}\n\ndata: [DONE]\n\n"
s := newTestStream(input)
defer s.Close()
if !s.Next() {
t.Fatalf("expected Next() to return true, err: %v", s.Err())
}
chunk := s.Current()
if chunk.ID != "1" || chunk.Content != "hello" {
t.Errorf("got %+v", chunk)
}
if s.Next() {
t.Error("expected Next() to return false after [DONE]")
}
if s.Err() != nil {
t.Errorf("unexpected error: %v", s.Err())
}
}
func TestStream_MultipleChunks(t *testing.T) {
input := "data: {\"id\":\"1\",\"content\":\"a\"}\n\ndata: {\"id\":\"2\",\"content\":\"b\"}\n\ndata: {\"id\":\"3\",\"content\":\"c\"}\n\ndata: [DONE]\n\n"
s := newTestStream(input)
defer s.Close()
var chunks []testChunk
for s.Next() {
chunks = append(chunks, s.Current())
}
if s.Err() != nil {
t.Fatal(s.Err())
}
if len(chunks) != 3 {
t.Fatalf("got %d chunks, want 3", len(chunks))
}
if chunks[0].Content != "a" || chunks[1].Content != "b" || chunks[2].Content != "c" {
t.Errorf("got %+v", chunks)
}
}
func TestStream_EmptyStream(t *testing.T) {
s := newTestStream("data: [DONE]\n\n")
defer s.Close()
if s.Next() {
t.Error("expected no chunks before [DONE]")
}
if s.Err() != nil {
t.Errorf("unexpected error: %v", s.Err())
}
}
func TestStream_InvalidJSON(t *testing.T) {
input := "data: not-json\n\n"
s := newTestStream(input)
defer s.Close()
if s.Next() {
t.Error("expected Next() to return false for invalid JSON")
}
if s.Err() == nil {
t.Error("expected error for invalid JSON")
}
}
func TestStream_NextAfterDone(t *testing.T) {
input := "data: {\"id\":\"1\",\"content\":\"x\"}\n\ndata: [DONE]\n\n"
s := newTestStream(input)
defer s.Close()
s.Next() // consume first chunk
s.Next() // hits [DONE]
// Calling Next() again should still return false
if s.Next() {
t.Error("expected false after stream is done")
}
}
func TestStream_NextAfterError(t *testing.T) {
input := "data: bad\n\n"
s := newTestStream(input)
defer s.Close()
s.Next() // triggers error
// Calling Next() again should still return false
if s.Next() {
t.Error("expected false after error")
}
}
func TestStream_WithComments(t *testing.T) {
input := ": keep-alive\ndata: {\"id\":\"1\",\"content\":\"ok\"}\n\n: ping\ndata: [DONE]\n\n"
s := newTestStream(input)
defer s.Close()
if !s.Next() {
t.Fatalf("expected chunk, err: %v", s.Err())
}
if s.Current().Content != "ok" {
t.Errorf("got %q", s.Current().Content)
}
if s.Next() {
t.Error("expected done after [DONE]")
}
}
func TestStream_EOFWithoutDone(t *testing.T) {
input := "data: {\"id\":\"1\",\"content\":\"x\"}\n\n"
s := newTestStream(input)
defer s.Close()
if !s.Next() {
t.Fatalf("expected chunk, err: %v", s.Err())
}
if s.Next() {
t.Error("expected false at EOF")
}
if s.Err() != nil {
t.Errorf("expected no error at clean EOF, got: %v", s.Err())
}
}