package service import ( "bytes" "log/slog" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations" "git.vakhrushev.me/av/transcriber/internal/contract" "git.vakhrushev.me/av/transcriber/internal/entity" ) // Ответ отправителю уходит после того, как достигнутый рубеж сохранён. Значит, // недоставка не может быть отказом шага: объявленный отказ засчитался бы воркеру // сбоем, лёг бы владельцу записью отказа и переписал бы служебные поля // доведённой записи. Причин недоставки две, исход у них общий. // downSender изображает неподнятый канал доставки: так ведёт себя заглушка, // которую ядро получает вместо отправителя Telegram. type downSender struct { calls int } func (s *downSender) Send(string, int64, *int) error { s.calls++ return contract.ErrDeliveryChannelDown } // journalEnv пересобирает сервис с названным отправителем и своим журналом: // утверждения судят и состояние записи, и то, что увидел владелец. func journalEnv( t *testing.T, env *pipelineEnv, sender contract.TelegramMessageSender, ) (*TranscribeService, *bytes.Buffer) { t.Helper() journal := &bytes.Buffer{} svc := NewTranscribeService( env.repos, &okMetaViewer{}, &okConverter{}, env.service.recognizer, sender, testLimits, slog.New(slog.NewTextHandler(journal, &slog.HandlerOptions{Level: slog.LevelDebug})), ) return svc, journal } // transcribedRecord доводит запись до рубежа, с которого уходит ответ. func transcribedRecord(t *testing.T, env *pipelineEnv, svc *TranscribeService) *entity.AudioRecord { t.Helper() record := newTelegramRecord(t, env) for range 5 { clearDelay(t, env, record.Id) require.NoError(t, svc.RunStep(t.Context())) if readRecord(t, env, record.Id).State == entity.StateTranscribed { return readRecord(t, env, record.Id) } } t.Fatal("запись не дошла до рубежа расшифровки") return nil } // Канал не поднят: запись доводится до конца, шаг отказа не объявляет, а // владелец узнаёт о недоставке из журнала. func TestUndeliveredOnDownChannelKeepsRecordDone(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{}) sender := &downSender{} svc, journal := journalEnv(t, env, sender) record := transcribedRecord(t, env, svc) // Шаг завершается без отказа — именно это воркер считает в свой счётчик. clearDelay(t, env, record.Id) require.NoError(t, svc.RunStep(t.Context())) assert.Equal(t, 1, sender.calls, "ответ до отправителя доехал") after := readRecord(t, env, record.Id) assert.Equal(t, entity.StateDone, after.State, "запись осталась на достигнутом рубеже") assert.False(t, after.IsHalted(), "отказ записи не приписан") require.NotNil(t, after.TranscriptTextID, "расшифровка сохранена") text, err := env.repos.Texts.GetByID(*after.TranscriptTextID) require.NoError(t, err) require.NotEmpty(t, text.Contents) written := journal.String() assert.Contains(t, written, "Reply was not delivered", "недоставка названа") assert.Contains(t, written, record.Id, "запись несёт идентификатор") assert.Contains(t, written, "level=WARN", "объявленный режим — «может стать проблемой»") assert.NotContains(t, written, text.Contents, "текста расшифровки в журнале нет") } // Адресат у записи не назван: исход тот же. Прежде эта ветка объявляла отказ // шага на уже завершённой работе. func TestUndeliveredWithoutChatKeepsRecordDone(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{}) sender := &downSender{} svc, journal := journalEnv(t, env, sender) record := transcribedRecord(t, env, svc) // Запись из Telegram, у которой чат не назван: такую отдаёт правка в панели. // Колонка чистится мимо захвата — иначе подготовка унесла бы запись у шага. stored, err := env.app.FindRecordById(migrations.RecordsCollection, record.Id) require.NoError(t, err) stored.Set("tg_chat_id", nil) require.NoError(t, env.app.Save(stored)) clearDelay(t, env, record.Id) require.NoError(t, svc.RunStep(t.Context())) assert.Equal(t, 0, sender.calls, "до отправителя дело не дошло: адресата нет") after := readRecord(t, env, record.Id) assert.Equal(t, entity.StateDone, after.State) assert.False(t, after.IsHalted(), "отказ записи не приписан") written := journal.String() assert.Contains(t, written, "Reply was not delivered") assert.Contains(t, written, record.Id) assert.Contains(t, written, "chat is not specified", "причина названа") assert.Contains(t, written, "level=ERROR", "порча записи громче штатного «бот не настроен»: иначе сигнал утонет") } // Запись, принятая по HTTP, до отправителя не доходит вовсе: недоставки нет, и // записи о ней в журнале быть не должно — иначе журнал владельца заполнят // строки о записях основного входа. func TestApiRecordDoesNotReachSenderAndLogsNothing(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{}) record, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "voice.ogg", newOwner(t, env.app)) require.NoError(t, err) sender := &downSender{} svc, journal := journalEnv(t, env, sender) require.NoError(t, svc.send(record, "расшифровка записи")) assert.Equal(t, 0, sender.calls, "отправителя не звали") assert.NotContains(t, journal.String(), "Reply was not delivered", "недоставки не было") }