Команда 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>
96 lines
7.7 KiB
Markdown
96 lines
7.7 KiB
Markdown
## 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** команда отклоняется с конфликтом «торрент ещё качается», хардлинки
|
|
на неполные файлы не создаются, состояние загрузки не меняется
|