внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -14,10 +14,10 @@ import (
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
|
||||
// Ответ отправителю уходит после того, как достигнутое состояние сохранено.
|
||||
// Значит, недоставка не может быть отказом шага: объявленный отказ засчитался
|
||||
// бы воркеру сбоем, лёг бы владельцу записью отказа и переписал бы служебные
|
||||
// поля завершённой задачи. Причин недоставки две, исход у них общий.
|
||||
// Ответ отправителю уходит после того, как достигнутый рубеж сохранён. Значит,
|
||||
// недоставка не может быть отказом шага: объявленный отказ засчитался бы воркеру
|
||||
// сбоем, лёг бы владельцу записью отказа и переписал бы служебные поля
|
||||
// доведённой записи. Причин недоставки две, исход у них общий.
|
||||
|
||||
// downSender изображает неподнятый канал доставки: так ведёт себя заглушка,
|
||||
// которую ядро получает вместо отправителя Telegram.
|
||||
@@ -31,91 +31,105 @@ func (s *downSender) Send(string, int64, *int) error {
|
||||
}
|
||||
|
||||
// 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,
|
||||
env.repos,
|
||||
&okMetaViewer{},
|
||||
&failingConverter{},
|
||||
rec,
|
||||
&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 TestUndeliveredOnDownChannelKeepsJobDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
rec := &scriptedRecognizer{result: entity.NewInProgressResult()}
|
||||
job := transcribingJob(t, env, rec)
|
||||
func TestUndeliveredOnDownChannelKeepsRecordDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{})
|
||||
|
||||
rec.result = entity.NewCompletedResult()
|
||||
rec.text = "расшифровка записи"
|
||||
sender := &downSender{}
|
||||
svc, journal := journalEnv(t, env, rec, sender)
|
||||
svc, journal := journalEnv(t, env, sender)
|
||||
|
||||
record := transcribedRecord(t, env, svc)
|
||||
|
||||
// Шаг завершается без отказа — именно это воркер считает в свой счётчик.
|
||||
require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context()))
|
||||
clearDelay(t, env, record.Id)
|
||||
require.NoError(t, svc.RunStep(t.Context()))
|
||||
|
||||
assert.Equal(t, 1, sender.calls, "ответ до отправителя доехал")
|
||||
|
||||
after, err := readJob(env.app, job.Id)
|
||||
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)
|
||||
assert.Equal(t, entity.StateDone, after.State, "задача осталась в достигнутом состоянии")
|
||||
require.NotNil(t, after.TranscriptionText)
|
||||
assert.Equal(t, "расшифровка записи", *after.TranscriptionText, "расшифровка сохранена")
|
||||
assert.Nil(t, after.ErrorText, "отказ задаче не приписан")
|
||||
require.NotEmpty(t, text.Contents)
|
||||
|
||||
written := journal.String()
|
||||
assert.Contains(t, written, "Reply was not delivered", "недоставка названа")
|
||||
assert.Contains(t, written, job.Id, "запись несёт идентификатор задачи")
|
||||
assert.Contains(t, written, record.Id, "запись несёт идентификатор")
|
||||
assert.Contains(t, written, "level=WARN", "объявленный режим — «может стать проблемой»")
|
||||
assert.NotContains(t, written, "расшифровка записи", "текста расшифровки в журнале нет")
|
||||
assert.NotContains(t, written, text.Contents, "текста расшифровки в журнале нет")
|
||||
}
|
||||
|
||||
// Адресат у задачи не назван: исход тот же. Прежде эта ветка объявляла отказ
|
||||
// Адресат у записи не назван: исход тот же. Прежде эта ветка объявляла отказ
|
||||
// шага на уже завершённой работе.
|
||||
func TestUndeliveredWithoutChatKeepsJobDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
rec := &scriptedRecognizer{result: entity.NewInProgressResult()}
|
||||
job := transcribingJob(t, env, rec)
|
||||
func TestUndeliveredWithoutChatKeepsRecordDone(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{})
|
||||
|
||||
// Задача из 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)
|
||||
svc, journal := journalEnv(t, env, sender)
|
||||
|
||||
require.NoError(t, svc.FindAndRunTranscribeCheckJob(t.Context()))
|
||||
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, err := readJob(env.app, job.Id)
|
||||
require.NoError(t, err)
|
||||
after := readRecord(t, env, record.Id)
|
||||
assert.Equal(t, entity.StateDone, after.State)
|
||||
assert.Nil(t, after.ErrorText, "отказ задаче не приписан")
|
||||
assert.False(t, after.IsHalted(), "отказ записи не приписан")
|
||||
|
||||
written := journal.String()
|
||||
assert.Contains(t, written, "Reply was not delivered")
|
||||
assert.Contains(t, written, job.Id)
|
||||
assert.Contains(t, written, record.Id)
|
||||
assert.Contains(t, written, "chat is not specified", "причина названа")
|
||||
assert.Contains(t, written, "level=ERROR",
|
||||
"порча записи громче штатного «бот не настроен»: иначе сигнал утонет")
|
||||
@@ -123,17 +137,17 @@ func TestUndeliveredWithoutChatKeepsJobDone(t *testing.T) {
|
||||
|
||||
// Запись, принятая по HTTP, до отправителя не доходит вовсе: недоставки нет, и
|
||||
// записи о ней в журнале быть не должно — иначе журнал владельца заполнят
|
||||
// строки о задачах основного входа.
|
||||
func TestApiJobDoesNotReachSenderAndLogsNothing(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &failingConverter{})
|
||||
// строки о записях основного входа.
|
||||
func TestApiRecordDoesNotReachSenderAndLogsNothing(t *testing.T) {
|
||||
env := newPipelineEnv(t, &okMetaViewer{}, &okConverter{})
|
||||
|
||||
job, err := env.service.CreateJobFromApi(t.Context(), strings.NewReader("запись"), "voice.ogg", newOwner(t, env.app))
|
||||
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, &scriptedRecognizer{}, sender)
|
||||
svc, journal := journalEnv(t, env, sender)
|
||||
|
||||
require.NoError(t, svc.send(job, "расшифровка записи"))
|
||||
require.NoError(t, svc.send(record, "расшифровка записи"))
|
||||
|
||||
assert.Equal(t, 0, sender.calls, "отправителя не звали")
|
||||
assert.NotContains(t, journal.String(), "Reply was not delivered", "недоставки не было")
|
||||
|
||||
Reference in New Issue
Block a user