закрыта задача bulk-delete-page, заведены три задачи из урожая ревью

This commit is contained in:
av
2026-08-10 17:51:59 +03:00
parent 288be8ec34
commit b939192348
5 changed files with 191 additions and 65 deletions
@@ -0,0 +1,66 @@
# 🐞 Не сносить раздачу, которой владеет другая активная загрузка
- **Тип:** fix
- **Категория:** Ядро продукта
- **Зачем:** удаление старой закрытой задачи уничтожает файлы живой загрузки с тем же инфохэшем; воспроизведено падающим тестом на ревью bulk-delete-page
- **Теги:** goal:state-integrity
`Delete` зовёт `torrents/delete` с `deleteFiles=true` по хешам своей записи и не
спрашивает, не владеет ли этим инфохэшем другая **активная** загрузка. Человек
подтверждает удаление одной записи, а необратимое действие применяется к чужим
живым данным.
Двигает у цели `state-integrity` строку завершения «ни один известный сегодня
путь не оставляет состояние, которое не объясняется историей переходов»: живая
загрузка B уходит в `failed` по пропаже источника, и по её записи не видно, что
файлы снесла чужая операция.
## Воспроизведение
1. Довести задачу A по инфохэшу H до `done`.
2. Добавить тот же релиз заново: `CreateDownloadIfNoActive` ищет владельца
только среди активных, A терминальна, дедупа нет — заводится активная задача
B по тому же H. Файлы на диске у A и B общие, qBittorrent дедуплицирует
раздачу по хешу.
3. Удалить A — со страницы загрузки, из Telegram или со страницы группового
удаления.
Видно вместо ожидаемого: раздача снесена с файлами, B продолжает считать себя
качающейся и уходит в `failed` после дебаунса. Если B ещё не дошла до раскладки,
копии не остаётся нигде.
Прогнанные оракулы ревью (отчёт —
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`, находка
2): `TestAdversaryDeletablePageOffersHashOwnedByActiveDownload`,
`TestAdversaryDeleteWipesSourceOfAnotherActiveDownload`.
## Затрагивает
- `Worker.Delete` (`internal/worker/review.go`) — вызов `qbt.Delete`;
- предикат `store.FindActiveByInfohash` — существующий, новый заводить не надо;
- спека `state-reconciliation`, требование «Полное удаление загрузки
пользователем» — сегодня оно про такую проверку молчит;
- строка выбора и подтверждения на странице группового удаления, если решено
показывать признак чужого владения;
- qBittorrent `torrents/delete` с `deleteFiles=true` — необратимая операция.
## Критерии приёмки
- Удаление задачи, чьим инфохэшем владеет активная загрузка, отклоняется
конфликтом и раздачу не сносит (оракул: тест `internal/worker` — задача A в
`done` и активная B по тому же хешу, `Delete(A)` возвращает `ErrConflict`, у
подставного клиента qBittorrent ноль вызовов `Delete`).
- Отказ виден человеку причиной, а не общим «внутренняя ошибка» (оракул: тест
`internal/httpapi` — строка отчёта называет, что инфохэшем владеет активная
задача).
- Удаление задачи, чей инфохэш никем больше не занят, работает как прежде
(оракул: существующие тесты `Delete` остаются зелёными).
- Поведение записано дельта-спекой `state-reconciliation` (оракул:
`openspec validate --strict` и `task gate`).
## Рамки
Необратимое: снос раздачи с файлами. Проверка ставится в ядре, а не в
транспорте — допуск обязан держаться без транспорта. Правило дедупа при приёме
(«владелец ищется среди активных») эта задача не меняет: связывание v1/v2 и
доверие к паре `xt` разбирает `infohash-identity-integrity`.