Files
LocalAI/tests/e2e/mock_backend_test.go
Richard Palethorpe 8cd3f9fc47 feat(ui, openai): Structured errors and link to traces in error toast (#9068)
First when sending errors over SSE we now clearly identify them as such
instead of just sending the error string as a chat completion message.

We use this in the UI to identify errors and link to them to the traces.

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-03-20 15:06:07 +01:00

269 lines
9.0 KiB
Go

package e2e_test
import (
"context"
"io"
"net/http"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/openai/openai-go/v3"
)
var _ = Describe("Mock Backend E2E Tests", Label("MockBackend"), func() {
Describe("Text Generation APIs", func() {
Context("Predict (Chat Completions)", func() {
It("should return mocked response", func() {
resp, err := client.Chat.Completions.New(
context.TODO(),
openai.ChatCompletionNewParams{
Model: "mock-model",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Hello"),
},
},
)
Expect(err).ToNot(HaveOccurred())
Expect(len(resp.Choices)).To(Equal(1))
Expect(resp.Choices[0].Message.Content).To(ContainSubstring("mocked response"))
})
})
Context("PredictStream (Streaming Chat Completions)", func() {
It("should stream mocked tokens", func() {
stream := client.Chat.Completions.NewStreaming(
context.TODO(),
openai.ChatCompletionNewParams{
Model: "mock-model",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("Hello"),
},
},
)
hasContent := false
for stream.Next() {
response := stream.Current()
if len(response.Choices) > 0 && response.Choices[0].Delta.Content != "" {
hasContent = true
}
}
Expect(stream.Err()).ToNot(HaveOccurred())
Expect(hasContent).To(BeTrue())
})
})
})
Describe("Error Handling", func() {
Context("Non-streaming errors", func() {
It("should return error for request with error trigger", func() {
_, err := client.Chat.Completions.New(
context.TODO(),
openai.ChatCompletionNewParams{
Model: "mock-model",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("MOCK_ERROR"),
},
},
)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("simulated failure"))
})
})
Context("Streaming errors", func() {
It("should return error for streaming request with immediate error trigger", func() {
stream := client.Chat.Completions.NewStreaming(
context.TODO(),
openai.ChatCompletionNewParams{
Model: "mock-model",
Messages: []openai.ChatCompletionMessageParamUnion{
openai.UserMessage("MOCK_ERROR_IMMEDIATE"),
},
},
)
for stream.Next() {
// drain
}
Expect(stream.Err()).To(HaveOccurred())
})
It("should return structured error for mid-stream failure", func() {
body := `{"model":"mock-model","messages":[{"role":"user","content":"MOCK_ERROR_MIDSTREAM"}],"stream":true}`
req, err := http.NewRequest("POST", apiURL+"/chat/completions", strings.NewReader(body))
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
Expect(err).ToNot(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(200))
data, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
bodyStr := string(data)
// Should contain a structured error event
Expect(bodyStr).To(ContainSubstring(`"error"`))
Expect(bodyStr).To(ContainSubstring(`"message"`))
Expect(bodyStr).To(ContainSubstring("simulated mid-stream failure"))
// Should also contain [DONE]
Expect(bodyStr).To(ContainSubstring("[DONE]"))
})
})
})
Describe("Embeddings API", func() {
It("should return mocked embeddings", func() {
resp, err := client.Embeddings.New(
context.TODO(),
openai.EmbeddingNewParams{
Model: "mock-model",
Input: openai.EmbeddingNewParamsInputUnion{
OfArrayOfStrings: []string{"test"},
},
},
)
Expect(err).ToNot(HaveOccurred())
Expect(len(resp.Data)).To(Equal(1))
Expect(len(resp.Data[0].Embedding)).To(Equal(768))
})
})
Describe("TTS APIs", func() {
Context("TTS", func() {
It("should generate mocked audio", func() {
body := `{"model":"mock-model","input":"Hello world","voice":"default"}`
req, err := http.NewRequest("POST", apiURL+"/audio/speech", io.NopCloser(strings.NewReader(body)))
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
Expect(err).ToNot(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(200))
Expect(resp.Header.Get("Content-Type")).To(HavePrefix("audio/"), "TTS response should set an audio Content-Type")
data, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(len(data)).To(BeNumerically(">", 0), "TTS response body should be non-empty")
})
})
})
Describe("Sound Generation API", func() {
It("should generate mocked sound (simple mode)", func() {
body := `{"model_id":"mock-model","text":"a soft Bengali love song for a quiet evening","instrumental":false,"vocal_language":"bn"}`
req, err := http.NewRequest("POST", apiURL+"/sound-generation", io.NopCloser(strings.NewReader(body)))
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
Expect(err).ToNot(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(200))
Expect(resp.Header.Get("Content-Type")).To(HavePrefix("audio/"), "sound-generation response should set an audio Content-Type (pkg/audio normalization)")
data, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(len(data)).To(BeNumerically(">", 0), "sound-generation response body should be non-empty")
})
It("should generate mocked sound (advanced mode)", func() {
body := `{"model_id":"mock-model","text":"upbeat pop","caption":"A funky Japanese disco track","lyrics":"[Verse 1]\nTest lyrics","think":true,"bpm":120,"duration_seconds":225,"keyscale":"Ab major","language":"ja","timesignature":"4"}`
req, err := http.NewRequest("POST", apiURL+"/sound-generation", io.NopCloser(strings.NewReader(body)))
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
Expect(err).ToNot(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(200))
Expect(resp.Header.Get("Content-Type")).To(HavePrefix("audio/"), "sound-generation response should set an audio Content-Type (pkg/audio normalization)")
data, err := io.ReadAll(resp.Body)
Expect(err).ToNot(HaveOccurred())
Expect(len(data)).To(BeNumerically(">", 0), "sound-generation response body should be non-empty")
})
})
Describe("Image Generation API", func() {
It("should generate mocked image", func() {
req, err := http.NewRequest("POST", apiURL+"/images/generations", nil)
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
body := `{"model":"mock-model","prompt":"a cat"}`
req.Body = http.NoBody
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(body)), nil
}
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
Expect(resp.StatusCode).To(BeNumerically("<", 500))
}
})
})
Describe("Audio Transcription API", func() {
It("should return mocked transcription", func() {
req, err := http.NewRequest("POST", apiURL+"/audio/transcriptions", nil)
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "multipart/form-data")
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
Expect(resp.StatusCode).To(BeNumerically("<", 500))
}
})
})
Describe("Rerank API", func() {
It("should return mocked reranking results", func() {
req, err := http.NewRequest("POST", apiURL+"/rerank", nil)
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
body := `{"model":"mock-model","query":"test","documents":["doc1","doc2"]}`
req.Body = http.NoBody
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(body)), nil
}
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
Expect(resp.StatusCode).To(BeNumerically("<", 500))
}
})
})
Describe("Tokenization API", func() {
It("should return mocked tokens", func() {
req, err := http.NewRequest("POST", apiURL+"/tokenize", nil)
Expect(err).ToNot(HaveOccurred())
req.Header.Set("Content-Type", "application/json")
body := `{"model":"mock-model","text":"Hello world"}`
req.Body = http.NoBody
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(body)), nil
}
httpClient := &http.Client{Timeout: 30 * time.Second}
resp, err := httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
Expect(resp.StatusCode).To(BeNumerically("<", 500))
}
})
})
})