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, rec contract.AudioRecognizer, sender contract.TelegramMessageSender, ) (*TranscribeService, *bytes.Buffer) { t.Helper() journal := &bytes.Buffer{} svc := NewTranscribeService( env.jobRepo, env.fileRepo, &okMetaViewer{}, &failingConverter{}, rec, sender, slog.New(slog.NewTextHandler(journal, &slog.HandlerOptions{Level: slog.LevelDebug})), ) return svc, journal } // Канал не поднят: задача доводится до конца, шаг отказа не объявляет, а // владелец узнаёт о недоставке из журнала. func TestUndeliveredOnDownChannelKeepsJobDone(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) rec := &scriptedRecognizer{result: entity.NewInProgressResult()} job := transcribingJob(t, env, rec) rec.result = entity.NewCompletedResult() rec.text = "расшифровка записи" sender := &downSender{} svc, journal := journalEnv(t, env, rec, sender) // Шаг завершается без отказа — именно это воркер считает в свой счётчик. require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context())) assert.Equal(t, 1, sender.calls, "ответ до отправителя доехал") after, err := env.jobRepo.GetByID(job.Id) require.NoError(t, err) assert.Equal(t, entity.StateDone, after.State, "задача осталась в достигнутом состоянии") require.NotNil(t, after.TranscriptionText) assert.Equal(t, "расшифровка записи", *after.TranscriptionText, "расшифровка сохранена") assert.Nil(t, after.ErrorText, "отказ задаче не приписан") written := journal.String() assert.Contains(t, written, "Reply was not delivered", "недоставка названа") assert.Contains(t, written, job.Id, "запись несёт идентификатор задачи") assert.Contains(t, written, "level=WARN", "объявленный режим — «может стать проблемой»") assert.NotContains(t, written, "расшифровка записи", "текста расшифровки в журнале нет") } // Адресат у задачи не назван: исход тот же. Прежде эта ветка объявляла отказ // шага на уже завершённой работе. func TestUndeliveredWithoutChatKeepsJobDone(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) rec := &scriptedRecognizer{result: entity.NewInProgressResult()} job := transcribingJob(t, env, rec) // Задача из Telegram, у которой чат не назван: такую отдаёт правка в панели. // Колонка чистится мимо захвата — иначе setup унёс бы задачу у шага. record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id) require.NoError(t, err) record.Set("tg_chat_id", nil) require.NoError(t, env.app.Save(record)) rec.result = entity.NewCompletedResult() rec.text = "расшифровка записи" sender := &downSender{} svc, journal := journalEnv(t, env, rec, sender) require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context())) assert.Equal(t, 0, sender.calls, "до отправителя дело не дошло: адресата нет") after, err := env.jobRepo.GetByID(job.Id) require.NoError(t, err) assert.Equal(t, entity.StateDone, after.State) assert.Nil(t, after.ErrorText, "отказ задаче не приписан") written := journal.String() assert.Contains(t, written, "Reply was not delivered") assert.Contains(t, written, job.Id) assert.Contains(t, written, "chat is not specified", "причина названа") assert.Contains(t, written, "level=ERROR", "порча записи громче штатного «бот не настроен»: иначе сигнал утонет") } // Запись, принятая по HTTP, до отправителя не доходит вовсе: недоставки нет, и // записи о ней в журнале быть не должно — иначе журнал владельца заполнят // строки о задачах основного входа. func TestApiJobDoesNotReachSenderAndLogsNothing(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "voice.ogg") require.NoError(t, err) sender := &downSender{} svc, journal := journalEnv(t, env, &scriptedRecognizer{}, sender) require.NoError(t, svc.send(job, "расшифровка записи")) assert.Equal(t, 0, sender.calls, "отправителя не звали") assert.NotContains(t, journal.String(), "Reply was not delivered", "недоставки не было") }