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