Upload a recording (audio, or video with the camera off), get a private Swedish transcript, and chat about it. - ADR-033: RLS-scoped private transcript store; ffprobe/ffmpeg intake; gateway transcription (iguana kb-whisper, berget fallback); in-process single-replica worker; chat over the transcript (local models only). - ADR-034: optional two-engine mode (pianissimo + kb-whisper) reconciled by a LOCAL model; off unless TAPIR_RECONCILE_BASE_MODEL is set. - ADR-035: real progress (phases, running/quiet/lost), phase logging, every judged spot stored, merged segments. - Hardening from four adversarial reviews: errors logged and stored as kinds never text; per-engine deadlines; converted-length check; bounded queue; client hang-ups are not errors; strict local-alias check. - Dockerfile: static ffmpeg/ffprobe 7.0.2 pinned by sha256 (#34). Built and inspected by hand; CI does not build the image on a PR, so its first CI build is this merge. Uploads stay OFF until TAPIR_UPLOADS_ENABLED is set in the deployment. Closes #33, #34, #42, #43, #44. Co-authored-by: Mathias <mthbqv@gmail.com>
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
package llm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/errkind"
|
|
)
|
|
|
|
// A non-200 from the gateway is an HTTP status error: its kind can be logged and stored
|
|
// without its body, which can echo the prompt (adversarial review F3).
|
|
|
|
func failingGateway(t *testing.T, status int, body string) *httptest.Server {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(status)
|
|
_, _ = w.Write([]byte(body))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestClient_ANon200IsAnHTTPStatusErrorWhoseKindCarriesNoBody(t *testing.T) {
|
|
srv := failingGateway(t, http.StatusBadRequest, `{"error":"prompt was: Anders ansvarar för virket"}`)
|
|
|
|
_, err := New(srv.URL, "", "m", 5*time.Second).Complete(context.Background(), "s", "u")
|
|
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
if got := errkind.Of(err); got != "http_400" {
|
|
t.Errorf("errkind.Of = %q, want http_400", got)
|
|
}
|
|
var he *HTTPError
|
|
if !errors.As(err, &he) || he.Code != http.StatusBadRequest {
|
|
t.Errorf("error is not an *HTTPError with the status code: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClient_TheErrorTextIsUnchangedForCallersThatLogIt(t *testing.T) {
|
|
srv := failingGateway(t, http.StatusBadGateway, "upstream down")
|
|
|
|
_, err := New(srv.URL, "", "m", 5*time.Second).Complete(context.Background(), "s", "u")
|
|
|
|
if err == nil || err.Error() != "LLM returned 502: upstream down" {
|
|
t.Errorf("error text = %v: callers that log it already rely on this form", err)
|
|
}
|
|
}
|
|
|
|
func TestClient_AnEnormousErrorBodyIsBoundedInTheErrorText(t *testing.T) {
|
|
srv := failingGateway(t, http.StatusInternalServerError, strings.Repeat("x", 100_000))
|
|
|
|
_, err := New(srv.URL, "", "m", 5*time.Second).Complete(context.Background(), "s", "u")
|
|
|
|
if err == nil || len(err.Error()) > 1024 {
|
|
t.Errorf("error text is %d bytes: a gateway's whole body must not become an error message", len(err.Error()))
|
|
}
|
|
}
|