package service import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "git.vakhrushev.me/av/transcriber/internal/contract" "git.vakhrushev.me/av/transcriber/internal/entity" ) // killedConverter ведёт себя как настоящий `ffmpeg`, убитый по контексту: // дожидается отмены и отдаёт отказ, который **не** несёт `context.Canceled`. // Это не упрощение, а суть проверки: `exec` отдаёт `*exec.ExitError` с текстом // «signal: killed», и отличить остановку от негодной записи по самой ошибке // нельзя — только по контексту шага. type killedConverter struct { cancel func() } func (c *killedConverter) Convert(ctx context.Context, _, _ string) error { c.cancel() <-ctx.Done() return errors.New("ffmpeg conversion failed: signal: killed") } // Критерий приёмки 9. Остановка сервиса посреди приведения не выносит записи // приговора и **не тратит отказа**: запись не виновата в том, что нас // перезапустили, и несколько выкладок подряд иначе останавливают здоровую // многочасовую запись с приговором «попытки исчерпаны». func TestShutdownDuringConversionKeepsRecordRetryable(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() converter := &killedConverter{cancel: cancel} env := newPipelineEnv(t, &okMetaViewer{}, converter) record := newTelegramRecord(t, env) err := env.service.RunStep(ctx) require.Error(t, err, "шаг обязан сообщить об обрыве наверх") require.ErrorIs(t, err, context.Canceled, "обрыв узнаётся по смыслу, а не по тексту") after := readRecord(t, env, record.Id) assert.Equal(t, entity.StateUploaded, after.State, "запись осталась на повтор") assert.False(t, after.IsHalted(), "приговора не выносили") assert.Nil(t, after.AcquisitionID, "захват снят: запись возьмёт следующий прогон") assert.Equal(t, 0, after.Attempts, "остановка отказа не тратит") assert.Nil(t, after.ErrorText) assert.Empty(t, env.sender.sent(), "отправителю о несуществующем сбое не сообщают") } // Запись, которую шаг не успел взять, потому что нас уже остановили, остаётся // нетронутой: захват не случился, отказ не потрачен. func TestShutdownBeforeStepLeavesRecordUntouched(t *testing.T) { env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{}) record := newTelegramRecord(t, env) ctx, cancel := context.WithCancel(t.Context()) cancel() err := env.service.RunStep(ctx) // Исход «шаг не сделал ничего» — это `NoopJobError`: воркер не пишет о нём // владельцу и не считает его в метрику. require.Error(t, err) var noop *contract.NoopJobError require.ErrorAs(t, err, &noop) after := readRecord(t, env, record.Id) assert.Equal(t, entity.StateUploaded, after.State) assert.Equal(t, 0, after.Attempts, "захвата не было — отказу взяться неоткуда") assert.Nil(t, after.AcquisitionID) }