Команда Defer гардила только IsTerminal() и потому принимала catched (торрент ещё не добавлен в qBittorrent). Defer из catched уводил задачу в лимбо → необратимый deleted: processCatched листает только catched и больше её не подхватывал, а последующие команды через отсутствие источника выводили deleted (ноль исходящих рёбер), хотя байты .torrent лежат в download_torrent. - Worker.Defer отклоняет catched с ErrConflict (транслируется в 409 / редирект с сообщением); прочие не-терминальные состояния, где раздача уже есть, принимает как раньше. - Снято мёртвое ребро графа catched → deferred (allowedTransitions); инвариант «deferred из каждого не-терминального» уточнён: кроме пре-источникового catched. catched — единственное состояние без раздачи среди не-терминальных. - Тесты: Defer из catched отклоняется и не меняет состояние; инвариант графа обновлён + негативная проверка ребра. - OpenSpec: MODIFIED «Команды ревью и их эффекты» (review) с позитивным и негативным сценариями; change заархивирован, дельта влита в спеку. - Беклог: закрыта review-major6-defer-catched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.7 KiB
MODIFIED Requirements
Requirement: Команды ревью и их эффекты
Экран ревью SHALL предоставлять команды: Применить (создать хардлинки по
эффективному плану), Уточнить (добавить подсказку → перераспознать),
Распознать заново (повторный прогон без новой подсказки), Игнор файла,
Позже (deferred), Отклонить (cancelled), Undo (снять созданные
ссылки → reverted) и Привязать заново (из
reverted/cancelled/target_missing → перераспознавание с ручным
подтверждением). Экран ревью MUST NOT содержать команду переключения типа
movie↔series: тип показывается read-only, а его корректировка выполняется
мягкой подсказкой через Уточнить. Команды из любого транспорта SHALL
сериализоваться worker'ом под единой блокировкой; применяется последняя валидная
команда.
Команда Позже (Defer) SHALL парковать задачу в deferred из любого
не-терминального состояния, у которого уже есть раздача в qBittorrent, и SHALL
отклонять её из пре-источникового состояния catched (торрент ещё НЕ
добавлен в qBittorrent) — конфликтом (ErrConflict) с понятным пользователю
сообщением, НЕ меняя состояние загрузки. Пре-источниковое catched —
единственное состояние без раздачи среди не-терминальных: откладывать в нём
нечего (задача ещё не дошла до ревью), а catched → deferred уводил бы задачу в
лимбо — processCatched листает только catched и больше её не подхватит, а
последующие команды через отсутствие источника выводят необратимый deleted.
Терминальные состояния Defer SHALL отклонять как и прежде (ErrConflict).
Команды, которым нужен источник (Применить, Уточнить, Распознать
заново, Привязать заново, а также фиксация типа), SHALL синхронно (без
дебаунса) проверять перед действием, что источник не только присутствует в
qBittorrent, но и готов к раскладке — раздача в готовом классе состояния
(uploading/stalledUP/pausedUP/… с учётом различий имён qBit v4/v5),
т.е. файлы докачаны. Если источник ещё качается (любое downloading-подобное
или переходное moving/checking состояние), команда SHALL отказывать с
конфликтом и причиной «торрент ещё качается», НЕ создавая хардлинки и НЕ меняя
состояние загрузки (её нахождение в review/deferred/… легитимно, приводить
к реальности нечего). Отсутствие источника в qBittorrent SHALL по-прежнему
приводить состояние к реальности (orphaned/deleted) и отказывать. Так
недокачанная задача не может пройти через перераспознавание в авто-раскладку
или ручное применение и захардлинкать неполные файлы, обойдя финальность
состояния completed.
Scenario: Применение создаёт раскладку
- GIVEN загрузка в
reviewс эффективным планом - WHEN пользователь выбирает «Применить»
- THEN создаются хардлинки по плану, задача переходит к раскладке
Scenario: Отклонить и привязать заново
- GIVEN загрузка в
review - WHEN пользователь «Отклонить», затем «Привязать заново»
- THEN задача уходит в
cancelled, а затем снова на распознавание с ручным подтверждением (авто-раскладка не делается)
Scenario: Тип не переключается кнопкой
- GIVEN загрузка в
reviewс распознанным типом - WHEN пользователь открывает экран ревью
- THEN отдельной команды/кнопки переключения movie↔series на экране нет
- AND тип показан read-only в инфо-части выбранного источника
Scenario: Позже паркует задачу из ревью
- GIVEN загрузка в
review(раздача в qBittorrent уже есть) - WHEN пользователь выбирает «Позже»
- THEN задача переходит в
deferredи возвращается на поверхность ревью по любому последующему действию
Scenario: Позже отклоняется для пре-источникового catched
- GIVEN загрузка в
catched(торрент ещё не добавлен в qBittorrent) - WHEN приходит команда «Позже» (
Defer, напр. прямым POST на/ui/downloads/{id}/defer) - THEN команда отклоняется конфликтом с понятным сообщением, что отложить можно только после добавления торрента
- AND загрузка остаётся в
catchedи штатно доходит доdownloadingчерезprocessCatched
Scenario: Недокачанный источник отклоняет перераспознавание
- GIVEN загрузка припаркована в
deferred, а её раздача в qBittorrent ещё качается (downloading, файлы не докачаны) - WHEN пользователь выбирает «Распознать заново» (или «Уточнить»/«Привязать заново»/фиксацию типа)
- THEN команда отклоняется с конфликтом и причиной «торрент ещё качается»
- AND загрузка остаётся в
deferred, хардлинки не создаются, авто-раскладка не запускается
Scenario: Недокачанный источник отклоняет ручное применение
- GIVEN загрузка в
review, чья раздача в qBittorrent ещё качается - WHEN пользователь выбирает «Применить»
- THEN команда отклоняется с конфликтом «торрент ещё качается», хардлинки на неполные файлы не создаются, состояние загрузки не меняется