package main import ( "context" "errors" "os" "path/filepath" "testing" "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/config" "git.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/web" ) // fakeUploadsStore satisfies the store surface buildUploads needs, and counts the // start-up sweep. type fakeUploadsStore struct { sweeps int sweepErr error } func (f *fakeUploadsStore) CreateUpload(context.Context, string, string, bool) (string, error) { return "id", nil } func (f *fakeUploadsStore) FailUpload(context.Context, string, string, domain.UploadErrorKind) error { return nil } func (f *fakeUploadsStore) StartTranscribing(context.Context, string, string) error { return nil } func (f *fakeUploadsStore) CompleteUpload(context.Context, string, string, domain.Transcription) error { return nil } func (f *fakeUploadsStore) FailInterruptedUploads(context.Context) (int, error) { f.sweeps++ return 2, f.sweepErr } func uploadCfg(t *testing.T, enabled bool) config.Config { t.Helper() var cfg config.Config cfg.GatewayURL = "http://gateway.invalid/v1" cfg.UploadsEnabled = enabled cfg.UploadMaxBytes = 200 << 20 cfg.UploadTmpDir = filepath.Join(t.TempDir(), "spool") cfg.TranscribeModel = "iguana/kb-whisper" cfg.TranscribeFallbackModel = "berget/kb-whisper" return cfg } func TestBuildUploads_DisabledMountsNothingAndSweepsNothing(t *testing.T) { st := &fakeUploadsStore{} in, w, err := buildUploads(context.Background(), uploadCfg(t, false), st, quietLog()) require.NoError(t, err) assert.Nil(t, in, "a typed-nil Intake must never reach web.App.Uploads: it would mount the routes over nothing") assert.Nil(t, w) assert.Zero(t, st.sweeps) } func TestBuildUploads_EnabledFailsInterruptedRowsBeforeAnythingElseStarts(t *testing.T) { st := &fakeUploadsStore{} in, w, err := buildUploads(context.Background(), uploadCfg(t, true), st, quietLog()) require.NoError(t, err) require.NotNil(t, in) require.NotNil(t, w) assert.Equal(t, 1, st.sweeps, "audio is not retained, so rows left queued/transcribing by a restart can never resume: fail them at start-up, not never") } func TestBuildUploads_SweepFailureStopsStartup(t *testing.T) { st := &fakeUploadsStore{sweepErr: errors.New("db down")} _, _, err := buildUploads(context.Background(), uploadCfg(t, true), st, quietLog()) require.Error(t, err) assert.Contains(t, err.Error(), "db down") } func TestBuildUploads_CreatesTheSpoolDirectoryPrivately(t *testing.T) { cfg := uploadCfg(t, true) _, _, err := buildUploads(context.Background(), cfg, &fakeUploadsStore{}, quietLog()) require.NoError(t, err) info, err := os.Stat(cfg.UploadTmpDir) require.NoError(t, err) assert.True(t, info.IsDir()) assert.Equal(t, os.FileMode(0o700), info.Mode().Perm(), "spooled audio is private third-party content") } func TestBuildUploads_EnabledWithoutAGatewayIsAnError(t *testing.T) { cfg := uploadCfg(t, true) cfg.GatewayURL = "" _, _, err := buildUploads(context.Background(), cfg, &fakeUploadsStore{}, quietLog()) assert.Error(t, err, "uploads on with nowhere to transcribe must fail loudly at start-up, not at the first upload") } func TestBuildUploads_RemovesAudioLeftBehindByACrash(t *testing.T) { cfg := uploadCfg(t, true) require.NoError(t, os.MkdirAll(cfg.UploadTmpDir, 0o700)) leftovers := []string{"upload-123", "wav-456"} for _, n := range leftovers { require.NoError(t, os.WriteFile(filepath.Join(cfg.UploadTmpDir, n), []byte("audio"), 0o600)) } unrelated := filepath.Join(cfg.UploadTmpDir, "someone-elses-file") require.NoError(t, os.WriteFile(unrelated, []byte("keep"), 0o600)) _, _, err := buildUploads(context.Background(), cfg, &fakeUploadsStore{}, quietLog()) require.NoError(t, err) for _, n := range leftovers { _, err := os.Stat(filepath.Join(cfg.UploadTmpDir, n)) assert.True(t, os.IsNotExist(err), "%s: audio is never retained, so a spooled file from a crashed run must not survive start-up", n) } _, err = os.Stat(unrelated) assert.NoError(t, err, "only our own spool files are removed") } func TestWireUploads_SetsEverythingTheWebAppNeedsForUploadsAndChat(t *testing.T) { cfg := uploadCfg(t, true) cfg.UploadMaxDuration = 90 * 60 * 1e9 cfg.MaxUploadedTranscriptChars = 90000 cfg.UploadChatModel = "iguana/gemma4-26b" in, _, err := buildUploads(context.Background(), cfg, &fakeUploadsStore{}, quietLog()) require.NoError(t, err) app := &web.App{} wireUploads(app, cfg, in) assert.NotNil(t, app.Uploads) assert.Equal(t, cfg.UploadMaxBytes, app.UploadLimits.MaxBytes, "the form states the limits Intake enforces") assert.Equal(t, cfg.UploadMaxDuration, app.UploadLimits.MaxDuration) assert.Equal(t, 90000, app.UploadChatMaxChars, "without this the chat would use the 18,000-char caption default and cut an hour to its first third") assert.Equal(t, "iguana/gemma4-26b", app.UploadChatModel) } func TestWireUploads_NilIntakeLeavesTheAppUntouched(t *testing.T) { app := &web.App{} wireUploads(app, uploadCfg(t, false), nil) assert.Nil(t, app.Uploads, "a nil *Intake must not become a non-nil interface that mounts the routes") assert.Zero(t, app.UploadChatMaxChars) } func TestBuildTranscriber_DefaultIsTheSingleEngineChainAsBefore(t *testing.T) { cfg := uploadCfg(t, true) tr := buildTranscriber(cfg) assert.IsType(t, &transcribe.Chain{}, tr, "with no base engine configured nothing changes") } func TestBuildTranscriber_ABaseEngineTurnsOnTheEnsemble(t *testing.T) { cfg := uploadCfg(t, true) cfg.ReconcileBaseModel = "iguana/pianissimo" cfg.ReconcileModel = "iguana/gemma4-26b" tr := buildTranscriber(cfg) assert.IsType(t, &transcribe.Ensemble{}, tr) } func TestBuildUploads_WiresTheEnsembleWhenConfigured(t *testing.T) { cfg := uploadCfg(t, true) cfg.ReconcileBaseModel = "iguana/pianissimo" cfg.ReconcileModel = "iguana/gemma4-26b" in, w, err := buildUploads(context.Background(), cfg, &fakeUploadsStore{}, quietLog()) require.NoError(t, err) assert.NotNil(t, in) assert.NotNil(t, w) }