внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer"
|
||||
pbrepo "git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/pocketbase/migrations"
|
||||
"git.vakhrushev.me/av/transcriber/internal/clock"
|
||||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
@@ -167,8 +169,16 @@ func setupTestEnv(t *testing.T, metaviewer contract.AudioMetaViewer) *testEnv {
|
||||
|
||||
pbrepo.BindPanelRules(app)
|
||||
|
||||
fileRepo := pbrepo.NewFileRepository(app)
|
||||
jobRepo := pbrepo.NewTranscriptJobRepository(app)
|
||||
recordRepo := pbrepo.NewAudioRecordRepository(app)
|
||||
textRepo := pbrepo.NewTextRepository(app)
|
||||
repos := service.Repositories{
|
||||
Records: recordRepo,
|
||||
Files: pbrepo.NewFileRepository(app),
|
||||
Texts: textRepo,
|
||||
Structures: pbrepo.NewStructureRepository(app),
|
||||
Recognitions: pbrepo.NewRecognitionRepository(app),
|
||||
Events: pbrepo.NewRecordEventRepository(app),
|
||||
}
|
||||
|
||||
// Журнал уходит в буфер, а не в никуда: по нему судит проверка запрета на
|
||||
// имя отправителя. Вывод прогона от этого не меняется — ERROR-строки ветки
|
||||
@@ -178,16 +188,16 @@ func setupTestEnv(t *testing.T, metaviewer contract.AudioMetaViewer) *testEnv {
|
||||
logger := slog.New(slog.NewTextHandler(journal, nil))
|
||||
|
||||
trsService := service.NewTranscribeService(
|
||||
jobRepo,
|
||||
fileRepo,
|
||||
repos,
|
||||
metaviewer,
|
||||
&stubConverter{},
|
||||
&recognizer.MemoryAudioRecognizer{},
|
||||
&TestTgSender{},
|
||||
entity.StuckLimits{Own: time.Hour, Foreign: 24 * time.Hour},
|
||||
logger,
|
||||
)
|
||||
|
||||
handler := NewTranscribeHandler(jobRepo, trsService, logger)
|
||||
handler := NewTranscribeHandler(recordRepo, textRepo, trsService, logger)
|
||||
|
||||
// Роутер собирается тем же способом, что и боевой: маршруты вешает сам
|
||||
// обработчик, и проверка судит ту же цепочку, что и прод.
|
||||
@@ -256,16 +266,16 @@ func countFiles(t *testing.T, env *testEnv) int {
|
||||
return len(records)
|
||||
}
|
||||
|
||||
// countJobs считает заведённые задачи расшифровки.
|
||||
// countJobs считает заведённые аудиозаписи.
|
||||
func countJobs(t *testing.T, env *testEnv) int {
|
||||
records, err := env.app.FindAllRecords(migrations.JobsCollection)
|
||||
records, err := env.app.FindAllRecords(migrations.RecordsCollection)
|
||||
require.NoError(t, err)
|
||||
return len(records)
|
||||
}
|
||||
|
||||
// jobWithFile заводит задачу вместе с её записью: ссылка на файл обязательна
|
||||
// схемой, потому что без неё задача не пройдёт ни одного шага.
|
||||
func jobWithFile(t *testing.T, env *testEnv) *entity.TranscribeJob {
|
||||
func jobWithFile(t *testing.T, env *testEnv) *entity.AudioRecord {
|
||||
t.Helper()
|
||||
|
||||
repo := pbrepo.NewFileRepository(env.app)
|
||||
@@ -276,17 +286,18 @@ func jobWithFile(t *testing.T, env *testEnv) *entity.TranscribeJob {
|
||||
// Владелец — учётная запись проверки: задача, пришедшая из веба, без
|
||||
// владельца больше не заводится, и фикстура без него описывала бы состояние,
|
||||
// которого в проде не бывает.
|
||||
file, err := repo.CreateLocal("sample.mp3", work, env.account.Id)
|
||||
file, err := repo.Create("sample.mp3", work, contract.FileMeta{Format: "mp3"}, env.account.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
job := &entity.TranscribeJob{
|
||||
State: entity.StateCreated,
|
||||
Source: entity.SourceApi,
|
||||
OwnerID: &env.account.Id,
|
||||
FileID: &file.Id,
|
||||
record := &entity.AudioRecord{
|
||||
State: entity.StateUploaded,
|
||||
StateEnteredAt: clock.Now(),
|
||||
Source: entity.SourceApi,
|
||||
OwnerID: &env.account.Id,
|
||||
OriginalFileID: &file.Id,
|
||||
}
|
||||
require.NoError(t, env.handler.jobRepo.Create(job))
|
||||
return job
|
||||
require.NoError(t, env.handler.recordRepo.Create(record))
|
||||
return record
|
||||
}
|
||||
|
||||
// storedContent читает содержимое файла из хранилища.
|
||||
@@ -327,21 +338,21 @@ func TestCreateTranscribeJob_Success(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, response.JobID)
|
||||
assert.Equal(t, entity.StateCreated, response.State)
|
||||
assert.Equal(t, entity.StateUploaded, response.State)
|
||||
|
||||
// Задача действительно заведена, а не только названа в ответе: иначе
|
||||
// отправитель получит идентификатор записи, которой не будет никогда.
|
||||
require.Equal(t, 1, countJobs(t, env))
|
||||
|
||||
job, err := env.handler.jobRepo.GetByID(response.JobID, env.account.Id)
|
||||
job, err := env.handler.recordRepo.GetByID(response.JobID, env.account.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, entity.StateCreated, job.State)
|
||||
require.NotNil(t, job.FileID)
|
||||
assert.NotEmpty(t, *job.FileID)
|
||||
assert.Equal(t, entity.StateUploaded, job.State)
|
||||
require.NotNil(t, job.OriginalFileID)
|
||||
assert.NotEmpty(t, *job.OriginalFileID)
|
||||
|
||||
// Содержимое лежит в хранилище одним файлом и целиком.
|
||||
require.Equal(t, 1, countFiles(t, env))
|
||||
assert.Equal(t, content, storedContent(t, env, *job.FileID))
|
||||
assert.Equal(t, content, storedContent(t, env, *job.OriginalFileID))
|
||||
}
|
||||
|
||||
func TestCreateTranscribeJob_NoFile(t *testing.T) {
|
||||
@@ -404,7 +415,7 @@ func TestCreateTranscribeJob_EmptyFile(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, response.JobID)
|
||||
assert.Equal(t, entity.StateCreated, response.State)
|
||||
assert.Equal(t, entity.StateUploaded, response.State)
|
||||
}
|
||||
|
||||
func TestCreateTranscribeJob_DifferentFileExtensions(t *testing.T) {
|
||||
@@ -513,7 +524,7 @@ const senderNameMarker = "SENDERNAMELEAKMARKER7Q2"
|
||||
// долг `docs/conventions/logging.md`: `msg` обязан стать короткой категорией.
|
||||
// Когда долг закроют, правка будет здесь и одна.
|
||||
const (
|
||||
msgIntake = "Creating transcribe job"
|
||||
msgIntake = "Creating audio record"
|
||||
// Отказ пишет доменная граница — приём, — а не транспорт: конвенция просит
|
||||
// логировать ошибку один раз, и повторная запись транспорта снята.
|
||||
msgIntakeErr = "Failed to get file info"
|
||||
@@ -603,15 +614,15 @@ func TestCreateTranscribeJob_JournalTracesRecord(t *testing.T) {
|
||||
var response CreateTranscribeJobResponse
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response))
|
||||
|
||||
job, err := env.handler.jobRepo.GetByID(response.JobID, env.account.Id)
|
||||
job, err := env.handler.recordRepo.GetByID(response.JobID, env.account.Id)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, job.FileID)
|
||||
require.NotNil(t, job.OriginalFileID)
|
||||
|
||||
// Отбор по идентификатору **этого** прогона: иначе утверждение прошло бы по
|
||||
// строке, оставленной соседней проверкой, и прослеживаемость числилась бы
|
||||
// сохранённой при пустом журнале.
|
||||
journal := env.journal.String()
|
||||
assert.Contains(t, journal, *job.FileID, "по журналу видно, какой файл заведён")
|
||||
assert.Contains(t, journal, *job.OriginalFileID, "по журналу видно, какой файл заведён")
|
||||
assert.Contains(t, journal, ".mp3", "расширение принятой записи в журнале остаётся")
|
||||
|
||||
// Разделитель ключа и значения задаёт обработчик: сегодня текстовый, по
|
||||
@@ -698,7 +709,7 @@ func TestGetTranscribeJobStatus_Success(t *testing.T) {
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response))
|
||||
|
||||
assert.Equal(t, job.Id, response.JobID)
|
||||
assert.Equal(t, entity.StateCreated, response.State)
|
||||
assert.Equal(t, entity.StateUploaded, response.State)
|
||||
assert.NotZero(t, response.CreatedAt)
|
||||
}
|
||||
|
||||
@@ -761,7 +772,7 @@ func TestAcceptedRecordSurvivesSenderDisconnect(t *testing.T) {
|
||||
|
||||
require.Equal(t, http.StatusCreated, w.Result().StatusCode, "тело ответа: %s", w.Body.String())
|
||||
|
||||
jobs, err := env.app.FindAllRecords(migrations.JobsCollection)
|
||||
jobs, err := env.app.FindAllRecords(migrations.RecordsCollection)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, jobs, 1, "задача заведена, несмотря на ушедшего отправителя")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user