Low findings of the PR #62 re-review: the WithTimeout test's comment claimed cancellation travels on the context but only the language was asserted (the WithoutCancel mutant survived), and PlanFor('') and PlanFor(unknown) were untested. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
147 lines
5.6 KiB
Go
147 lines
5.6 KiB
Go
package transcribe_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.d-ma.be/mathias/tapir/internal/adapters/transcribe"
|
|
"git.d-ma.be/mathias/tapir/internal/domain"
|
|
"git.d-ma.be/mathias/tapir/internal/errkind"
|
|
"git.d-ma.be/mathias/tapir/internal/reconcile"
|
|
)
|
|
|
|
// A wedged engine (iguana after a Metal OOM, infra#273) does not answer at all. With only
|
|
// the job's deadline, the primary used up the whole job before any fallback could run, and
|
|
// the chain then refused the fallback because the job context had expired. Each engine call
|
|
// gets its own deadline (adversarial review F4).
|
|
|
|
type hangingBackend struct{ name string }
|
|
|
|
func (h hangingBackend) Name() string { return h.name }
|
|
func (hangingBackend) Transcribe(ctx context.Context, _ string) (domain.Transcription, error) {
|
|
<-ctx.Done()
|
|
return domain.Transcription{}, ctx.Err()
|
|
}
|
|
|
|
func TestWithTimeout_AHungBackendFailsAtItsOwnDeadlineWhileTheJobIsStillAlive(t *testing.T) {
|
|
job, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
b := transcribe.WithTimeout(hangingBackend{name: "iguana/kb-whisper"}, 40*time.Millisecond)
|
|
|
|
start := time.Now()
|
|
_, err := b.Transcribe(job, "/a.wav")
|
|
|
|
require.Error(t, err)
|
|
assert.Equal(t, "timeout", errkind.Of(err))
|
|
assert.Less(t, time.Since(start), 2*time.Second, "it must give up at its own deadline, not wait for the job's")
|
|
assert.NoError(t, job.Err(), "the job context is untouched, which is what lets a fallback run")
|
|
assert.Equal(t, "iguana/kb-whisper", b.Name(), "the wrapper is invisible in the recorded endpoint")
|
|
}
|
|
|
|
func TestWithTimeout_ASuccessPassesStraightThrough(t *testing.T) {
|
|
calls := &[]string{}
|
|
inner := &orderedBackend{name: "iguana/kb-whisper", text: "hej", calls: calls}
|
|
|
|
got, err := transcribe.WithTimeout(inner, time.Minute).Transcribe(context.Background(), "/a.wav")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hej", got.Text)
|
|
}
|
|
|
|
func TestWithTimeout_ZeroMeansNoDeadlineOfItsOwn(t *testing.T) {
|
|
inner := &orderedBackend{name: "x", text: "hej", calls: &[]string{}}
|
|
|
|
assert.Same(t, inner, transcribe.WithTimeout(inner, 0), "a zero timeout must not wrap or change anything")
|
|
}
|
|
|
|
func TestChain_AHungPrimaryReachesTheFallbackWhenItHasItsOwnDeadline(t *testing.T) {
|
|
calls := &[]string{}
|
|
fb := &orderedBackend{name: "berget/kb-whisper", text: "fallback text", calls: calls}
|
|
c := transcribe.NewChain(transcribe.WithTimeout(hangingBackend{name: "iguana/kb-whisper"}, 40*time.Millisecond), fb)
|
|
job, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
got, err := c.Transcribe(job, "/a.wav")
|
|
|
|
require.NoError(t, err, "a wedged primary is exactly what the fallback exists for")
|
|
assert.Equal(t, "berget/kb-whisper", got.Endpoint)
|
|
}
|
|
|
|
func TestChain_AJobThatEndedWhileThePrimaryHungStillNeverReachesTheFallback(t *testing.T) {
|
|
calls := &[]string{}
|
|
fb := &orderedBackend{name: "berget/kb-whisper", text: "fallback text", calls: calls}
|
|
c := transcribe.NewChain(transcribe.WithTimeout(hangingBackend{name: "iguana/kb-whisper"}, time.Minute), fb)
|
|
job, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := c.Transcribe(job, "/a.wav")
|
|
|
|
require.Error(t, err)
|
|
assert.Empty(t, *calls, "the job's own deadline or a cancel must still stop the audio from reaching a cloud endpoint")
|
|
}
|
|
|
|
func TestEnsemble_AHungEngineIsASoftFailureAndTheOtherEnginesTextIsUsed(t *testing.T) {
|
|
calls := &[]string{}
|
|
alt := &orderedBackend{name: "iguana/kb-whisper", text: "text from the other engine", calls: calls}
|
|
e := transcribe.NewEnsemble(
|
|
transcribe.WithTimeout(hangingBackend{name: "iguana/pianissimo"}, 40*time.Millisecond), alt, nil,
|
|
&verdictAdj{v: reconcile.VerdictAlt}, ensembleOpts)
|
|
job, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
got, err := e.Transcribe(job, "/a.wav")
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "text from the other engine", got.Text)
|
|
assert.Equal(t, domain.ReconcileSingle, got.Reconciliation.Status)
|
|
assert.Contains(t, got.Reconciliation.Reason, "timeout")
|
|
assert.False(t, errors.Is(err, context.DeadlineExceeded))
|
|
}
|
|
|
|
// WithTimeout sits in front of both local engines in production (cmd/tapir/uploads.go). It
|
|
// must hand the engine the job's context, not a fresh one: the language and the
|
|
// cancellation both travel on it (tapir#61).
|
|
|
|
func TestWithTimeout_TheEngineStillReceivesTheLanguageOfTheJob(t *testing.T) {
|
|
for name, tt := range map[string]struct {
|
|
ctx context.Context
|
|
wantField bool
|
|
}{
|
|
"auto leaves the field out": {domain.WithLanguage(context.Background(), domain.LangAuto), false},
|
|
"swedish sends sv": {domain.WithLanguage(context.Background(), domain.LangSwedish), true},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
var got seen
|
|
srv := serve(t, 200, verboseJSON, &got)
|
|
b := transcribe.WithTimeout(transcribe.NewGateway(srv.URL+"/v1", "k", "iguana/kb-whisper", srv.Client()), time.Minute)
|
|
|
|
_, err := b.Transcribe(tt.ctx, wavFile(t, "RIFF-synthetic-pcm"))
|
|
|
|
require.NoError(t, err)
|
|
lang, present := got.fields["language"]
|
|
assert.Equal(t, tt.wantField, present)
|
|
if tt.wantField {
|
|
assert.Equal(t, "sv", lang)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWithTimeout_ACancelledJobIsNotRestartedByTheWrapper(t *testing.T) {
|
|
var got seen
|
|
srv := serve(t, 200, verboseJSON, &got)
|
|
b := transcribe.WithTimeout(transcribe.NewGateway(srv.URL+"/v1", "k", "iguana/kb-whisper", srv.Client()), time.Minute)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := b.Transcribe(ctx, wavFile(t, "RIFF-synthetic-pcm"))
|
|
|
|
require.Error(t, err)
|
|
assert.Empty(t, got.method, "a cancelled job must not send its audio anywhere through the wrapper")
|
|
}
|