Приём/UI: пачка фиксов границ и парсинга (F7–F10, N2)

Пять независимых bugfix'ов из ревью приёма (docs/backlog/review-f7-f10-ingest-ui-fixes.md):

- F7: oversized .torrent через веб отдавал 500. Введён sentinel
  ingest.ErrTorrentTooLarge, classifyErr транслирует его в 400.
- F8: гонка fast-path attach с cancel. Пред-рид
  FindReingestBlockingByInfohash больше не короткозамыкает активную
  запись — авторитетное дедуп-решение принимает CreateDownloadIfNoActive
  под BEGIN IMMEDIATE; короткозамыкание оставлено только для терминальных
  desync-записей (target_missing/orphaned). F6-апгрейд сохранён.
- F9: magnet — регистронезависимый URN-префикс xt (RFC 2141);
  tgbot.ParseMessage срезает хвостовую пунктуацию, приклеенную жадным
  matchем.
- F10: cap контекста до 16 KiB в ingest.Ingest (единственное место
  слияния — покрывает все транспорты), рунобезопасная обрезка + маркер.
- N2: httpapi.shorten режет по рунам, не байтам — кириллица не рвётся в
  U+FFFD.

Добавлены юнит-тесты на каждое исправленное поведение.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-17 20:35:14 +03:00
co-authored by Claude Opus 4.8
parent 17d9f23630
commit b8017d65eb
9 changed files with 244 additions and 15 deletions
+9
View File
@@ -13,6 +13,13 @@ var (
parenURL = regexp.MustCompile(`\s*\(https?://[^)]+\)`)
)
// magnetTrailingPunct — хвостовая пунктуация, которую жадный magnetRe (`[^\s]+`)
// приклеивает к ссылке, когда magnet стоит в конце предложения или в скобках
// («…magnet:?xt=…).»). Такой хвост сдвигает последний xt и ломает разбор
// (длина btih становится 41 → отказ). TrimRight трактует cutset как множество
// рун, поэтому многобайтовая «»» отсекается корректно.
const magnetTrailingPunct = ".,;:)]}>»"
// noisePrefixes — начала строк UI торрент-бота, которые в контекст не несём.
var noisePrefixes = []string{
"Открыть magnet", "или получить .torrent", "Оценить", "Следить",
@@ -29,6 +36,8 @@ func ParseMessage(text string) (source, context string, ok bool) {
if m == "" {
return "", "", false
}
// Срезаем хвостовую пунктуацию, приклеенную жадным матчем (см. константу).
m = strings.TrimRight(m, magnetTrailingPunct)
return m, cleanContext(text, m), true
}
+27
View File
@@ -75,3 +75,30 @@ func TestParseMessage_NoMagnet(t *testing.T) {
t.Error("ожидалось ok=false без magnet")
}
}
// F9: жадный magnetRe приклеивает хвостовую пунктуацию (magnet в конце
// предложения/в скобках) — ParseMessage её срезает, иначе последний xt сдвигается
// и длина btih ломается.
func TestParseMessage_TrimsTrailingPunct(t *testing.T) {
const want = "magnet:?xt=urn:btih:7931AA3ED6666746012F5739D099B5BC64D72A16"
cases := []struct {
name string
text string
}{
{"точка", "Смотри: " + want + "."},
{"скобка", "(" + want + ")"},
{"кавычка-ёлочка", "«" + want + "»"},
{"несколько", want + ").,"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
src, _, ok := ParseMessage(tc.text)
if !ok {
t.Fatal("magnet не найден")
}
if src != want {
t.Errorf("src = %q, want %q", src, want)
}
})
}
}