внутренняя модель перестроена вокруг аудиозаписи

- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
av
2026-08-14 20:20:33 +03:00
parent d079f03350
commit 1576d06735
84 changed files with 8973 additions and 2865 deletions
+23 -28
View File
@@ -8,7 +8,6 @@ import (
"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"
)
@@ -28,43 +27,43 @@ func (c *killedConverter) Convert(ctx context.Context, _, _ string) error {
return errors.New("ffmpeg conversion failed: signal: killed")
}
// Остановка сервиса посреди конвертации не выносит записи приговора: задача
// остаётся пригодной к повтору, попытку не тратит и отправителю о сбое,
// которого не было, не сообщает. Прежде любой отказ `Convert` уводил задачу в
// терминальное `failed`, откуда её возвращает только владелец правкой в панели.
func TestShutdownDuringConversionKeepsJobRetryable(t *testing.T) {
// Критерий приёмки 9. Остановка сервиса посреди приведения не выносит записи
// приговора и **не тратит отказа**: запись не виновата в том, что нас
// перезапустили, и несколько выкладок подряд иначе останавливают здоровую
// многочасовую запись с приговором «попытки исчерпаны».
func TestShutdownDuringConversionKeepsRecordRetryable(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
converter := &killedConverter{cancel: cancel}
env := newPipelineEnv(t, &okMetaViewer{}, converter)
job := newTelegramJob(t, env)
record := newTelegramRecord(t, env)
err := env.service.FindAndRunConversionJob(ctx)
err := env.service.RunStep(ctx)
require.Error(t, err, "шаг обязан сообщить об обрыве наверх")
require.ErrorIs(t, err, context.Canceled, "обрыв узнаётся по смыслу, а не по тексту")
after, err := readJob(env.app, job.Id)
require.NoError(t, err)
after := readRecord(t, env, record.Id)
assert.Equal(t, entity.StateCreated, after.State, "задача осталась на повтор, а не похоронена")
assert.Nil(t, after.AcquisitionID, "захват снят: задачу возьмёт следующий прогон")
assert.Equal(t, 0, after.Attempts, "остановка попытки не тратит")
assert.Nil(t, after.ErrorText, "приговора не выносили")
assert.Empty(t, env.sender.messages, "отправителю о несуществующем сбое не сообщают")
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 TestShutdownBeforeStepLeavesJobUntouched(t *testing.T) {
// Запись, которую шаг не успел взять, потому что нас уже остановили, остаётся
// нетронутой: захват не случился, отказ не потрачен.
func TestShutdownBeforeStepLeavesRecordUntouched(t *testing.T) {
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
job := newTelegramJob(t, env)
record := newTelegramRecord(t, env)
ctx, cancel := context.WithCancel(t.Context())
cancel()
err := env.service.FindAndRunConversionJob(ctx)
err := env.service.RunStep(ctx)
// Исход «шаг не сделал ничего» — это `NoopJobError`: воркер не пишет о нём
// владельцу и не считает его в метрику.
@@ -72,12 +71,8 @@ func TestShutdownBeforeStepLeavesJobUntouched(t *testing.T) {
var noop *contract.NoopJobError
require.ErrorAs(t, err, &noop)
after, err := readJob(env.app, job.Id)
require.NoError(t, err)
assert.Equal(t, entity.StateCreated, after.State)
assert.Equal(t, 0, after.Attempts, "захвата не было — попытке взяться неоткуда")
record, err := env.app.FindRecordById(migrations.JobsCollection, job.Id)
require.NoError(t, err)
assert.Empty(t, record.GetString("acquisition_id"))
after := readRecord(t, env, record.Id)
assert.Equal(t, entity.StateUploaded, after.State)
assert.Equal(t, 0, after.Attempts, "захвата не было — отказу взяться неоткуда")
assert.Nil(t, after.AcquisitionID)
}