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
+6 -4
View File
@@ -262,10 +262,12 @@ func (b *Bot) downloadFile(ctx context.Context, fileID string) ([]byte, error) {
func (b *Bot) ingestAndReply(ctx context.Context, chatID int64, req ingest.Request) {
res, err := b.ingestor.Ingest(ctx, req)
if err != nil {
// res.DownloadID непуст, если сбой после создания задачи (напр. qbit);
// при раннем разборе источника id ещё нет — даём дружелюбный текст без
// него (детали всё равно в логах на доменной границе).
b.send(chatID, opErr("Не удалось принять загрузку", res.DownloadID), nil)
// Приём на любом пути ошибки возвращает нулевой Result (контракт
// ingest.Ingest) — идентификатора загрузки нет. Корреляционного ключа
// у отказа приёма в Telegram нет вовсе (request_id — понятие
// HTTP-границы): текст остаётся дружелюбным без ключа, детали — в
// записи приёма на доменной границе (capability=ingest, infohash).
b.send(chatID, opErr("Не удалось принять загрузку", ""), nil)
return
}
if res.Deduplicated {
+29
View File
@@ -3,6 +3,7 @@ package tgbot
import (
"context"
"database/sql"
"errors"
"log/slog"
"strings"
"testing"
@@ -55,10 +56,15 @@ func (f *fakeAPI) GetFileDirectURL(string) (string, error)
type fakeIngestor struct {
lastReq ingest.Request
res ingest.Result
err error
}
func (f *fakeIngestor) Ingest(_ context.Context, req ingest.Request) (ingest.Result, error) {
f.lastReq = req
if f.err != nil {
// Контракт приёма: на ошибке результат нулевой (ingest.Ingest).
return ingest.Result{}, f.err
}
return f.res, nil
}
@@ -585,3 +591,26 @@ func TestBot_CallbackStaleButton(t *testing.T) {
t.Errorf("answers = %v, want понятный ответ", api.answers)
}
}
// Отказ приёма в Telegram: идентификатора загрузки нет (контракт ingest.Ingest —
// нулевой результат на любом пути ошибки), и обещать его пользователю нельзя.
// Корреляционного ключа у этого транспорта тоже нет — записанный вопрос
// изменения `ingest-nits`; тест фиксирует сегодняшнее состояние, чтобы
// «download_id=» не вернулся в текст молча.
func TestBot_IngestErrorPromisesNoDownloadID(t *testing.T) {
b, api, ing, _ := newTestBot(t, []int64{7})
ing.err = errors.New("boom")
b.handleMessage(context.Background(), msgFrom(7, "magnet:?xt=urn:btih:ABC"))
if len(api.sent) != 1 {
t.Fatalf("отправлено сообщений = %d, want 1", len(api.sent))
}
got := api.sent[0].text
if !strings.Contains(got, "Не удалось принять загрузку") {
t.Errorf("текст отказа = %q", got)
}
if strings.Contains(got, "download_id") || strings.Contains(got, idCode(tid)) {
t.Errorf("в отказе обещан идентификатор загрузки: %q", got)
}
}