67 lines
4.9 KiB
Markdown
67 lines
4.9 KiB
Markdown
# 🐞 Не сносить раздачу, которой владеет другая активная загрузка
|
||
|
||
- **Тип:** 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`.
|