package stt import ( "context" "errors" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "github.com/kami/maven/internal/audio" ) // scripted — a Transcriber that answers with a fixed text, or fails. type scripted struct { text string err error calls atomic.Int32 } func (s *scripted) Transcribe(_ context.Context, _ audio.Audio) (string, float64, error) { s.calls.Add(1) if s.err != nil { return "", 0, s.err } return s.text, 0.9, nil } func sample() audio.Audio { return audio.Audio{Format: audio.PCM16kMono, Bytes: make([]byte, 3200)} } // up builds a Pair whose admission answer is already true, without probing. func up(remote, floor Transcriber) *Pair { p := NewPair(remote, floor, "", time.Minute) p.up.Store(true) return p } func TestPairPrefersTheWorkstation(t *testing.T) { t.Parallel() remote := &scripted{text: "с рабочей станции"} floor := &scripted{text: "с homesrv"} text, _, err := up(remote, floor).Transcribe(context.Background(), sample()) if err != nil { t.Fatalf("Transcribe: %v", err) } if text != "с рабочей станции" { t.Fatalf("want the remote transcript, got %q", text) } if floor.calls.Load() != 0 { t.Fatalf("floor was called %d times, want 0", floor.calls.Load()) } } // The turn is what matters. A remote that dies mid-session must cost a worse // transcript and nothing else. This is the V-486 bar. func TestPairFallsBackWhenRemoteFails(t *testing.T) { t.Parallel() remote := &scripted{err: errors.New("connection refused")} floor := &scripted{text: "с homesrv"} p := up(remote, floor) text, conf, err := p.Transcribe(context.Background(), sample()) if err != nil { t.Fatalf("a failed remote must not fail the turn: %v", err) } if text != "с homesrv" { t.Fatalf("want the floor transcript, got %q", text) } if conf != 0.9 { t.Fatalf("want the floor confidence, got %v", conf) } if p.Available() { t.Fatal("a failed request must correct the cached admission answer") } // The next utterance goes straight to the floor rather than into the // same hole. if _, _, err := p.Transcribe(context.Background(), sample()); err != nil { t.Fatalf("second turn: %v", err) } if remote.calls.Load() != 1 { t.Fatalf("remote called %d times, want 1", remote.calls.Load()) } } func TestPairWithNoRemoteIsTheFloor(t *testing.T) { t.Parallel() floor := &scripted{text: "с homesrv"} p := NewPair(nil, floor, "", time.Minute) p.Start(context.Background()) // no health url, so this is a no-op if p.Available() { t.Fatal("an unconfigured remote is never available") } text, _, err := p.Transcribe(context.Background(), sample()) if err != nil { t.Fatalf("Transcribe: %v", err) } if text != "с homesrv" { t.Fatalf("want the floor transcript, got %q", text) } } func TestPairWithNoFloorRefuses(t *testing.T) { t.Parallel() _, _, err := NewPair(nil, nil, "", time.Minute).Transcribe(context.Background(), sample()) if !errors.Is(err, ErrNoFloor) { t.Fatalf("want ErrNoFloor, got %v", err) } } func TestPairProbeReadsHealth(t *testing.T) { t.Parallel() var ok atomic.Bool srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if !ok.Load() { w.WriteHeader(http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) })) defer srv.Close() p := NewPair(&scripted{text: "remote"}, &scripted{text: "floor"}, srv.URL, time.Minute) p.probe(context.Background()) if p.Available() { t.Fatal("a 503 means the card is busy, so the workstation is not available") } ok.Store(true) p.probe(context.Background()) if !p.Available() { t.Fatal("a 200 means the workstation will take work") } } func TestPairStopIsIdempotent(t *testing.T) { t.Parallel() p := NewPair(nil, &scripted{}, "", time.Minute) p.Stop() p.Stop() }