ingest: закрыты мелочи приёма — вырожденное имя, контракт Result, корреляция add

- имя раздачи нормализуется на границе разбора: вырожденное `-`
  (metainfo.NoName) даёт пустое имя, пробельное схлопывается — сентинел больше
  не доходит ни до контекста распознавания, ни до source_ref, ни до подсказки
  вывода имени
- контракт «на любом пути ошибки приёма результат нулевой» объявлен в ingest и
  удерживается структурно; три транспорта перестали обещать идентификатор,
  которого нет, и коррелируют отказ по request_id
- scoped-логгер загрузки ставится до вызова внешнего сервиса в семи командах
  воркера — записи об отказе qBittorrent и метабаз получили download_id
  и infohash; граница разбора bencode записана в docs/research
This commit is contained in:
av
2026-08-06 18:20:12 +03:00
parent 52615e4e49
commit d081ef1d30
30 changed files with 2190 additions and 63 deletions
+38
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"log/slog"
"reflect"
"strings"
"testing"
@@ -24,13 +25,22 @@ type fakeStore struct {
upgradeID string // downloadID последнего вызова UpgradeCatchedMagnetToTorrent
upgradeBlob []byte // байты, переданные в апгрейд
upgradeUp bool // что вернуть из UpgradeCatchedMagnetToTorrent
lookupErr error // отказ хранилища на дедуп-чеке
createErr error // отказ хранилища на заведении загрузки
}
func (f *fakeStore) FindReingestBlockingByInfohash(_ context.Context, _ ...string) (*store.Download, error) {
if f.lookupErr != nil {
return nil, f.lookupErr
}
return f.active, nil
}
func (f *fakeStore) CreateDownloadIfNoActive(_ context.Context, d *store.Download, hashes []string, torrentBlob []byte) (*store.Download, error) {
if f.createErr != nil {
return nil, f.createErr
}
if f.active != nil {
return f.active, nil
}
@@ -300,3 +310,31 @@ func TestIngestRejectsNonMagnet(t *testing.T) {
t.Error("не должно быть записи задачи")
}
}
// Контракт приёма: на ЛЮБОМ пути ошибки транспорту возвращается НУЛЕВОЙ Result.
// Транспорты на это опираются и не обещают идентификатора, которого нет
// (см. ingest-спеку, «Результат приёма при ошибке пуст»). Сравниваем результат
// с нулевым значением ЦЕЛИКОМ, а не по полю DownloadID: следующая ветвь отказа
// может заполнить другое поле.
func TestIngestReturnsZeroResultOnEveryErrorPath(t *testing.T) {
boom := errors.New("boom")
for _, tc := range []struct {
name string
fs *fakeStore
req Request
}{
{"невалидный источник", &fakeStore{}, Request{Source: "не magnet и не torrent"}},
{"сбой хранилища на дедуп-чеке", &fakeStore{lookupErr: boom}, Request{Source: sampleMagnet}},
{"сбой хранилища на заведении", &fakeStore{createErr: boom}, Request{Source: sampleMagnet}},
} {
t.Run(tc.name, func(t *testing.T) {
res, err := newService(tc.fs).Ingest(context.Background(), tc.req)
if err == nil {
t.Fatal("ожидалась ошибка")
}
if !reflect.DeepEqual(res, Result{}) {
t.Errorf("Result = %+v, want нулевой", res)
}
})
}
}