Files
mathiasandClaude Sonnet 5.5 f704258fdf
CI / Lint / Test / Vet (pull_request) Successful in 30s
CI / Build & Import (pull_request) Skipped
CI / Deploy via GitOps (pull_request) Skipped
test(transcribe): the wrapper does not restart a cancelled job; PlanFor of an empty or unknown language is pinned (#61)
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>
2026-10-04 22:48:29 +02:00

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")
}