закрыта задача bulk-delete-page, заведены три задачи из урожая ревью
This commit is contained in:
+3
-1
@@ -27,7 +27,8 @@
|
|||||||
- [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
|
- [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
|
||||||
- [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
|
- [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
|
||||||
- [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена
|
- [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена
|
||||||
- [✨ Удалять выбранные загрузки с файлами на отдельной странице](items/bulk-delete-page.md) — удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение
|
- [🐞 Не сносить раздачу, которой владеет другая активная загрузка](items/delete-checks-active-infohash-owner.md) — удаление старой закрытой задачи уничтожает файлы живой загрузки с тем же инфохэшем; воспроизведено падающим тестом на ревью bulk-delete-page
|
||||||
|
- [🐞 Не оставлять задачу в done, когда ссылки сняты, а раздача не снесена](items/delete-leaves-stale-done.md) — при недоступном qBittorrent задача весь простой соседа показывает done, хотя тайтла в Jellyfin уже нет: сверка падает на первом шаге и до коррекции не доходит
|
||||||
- [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
|
- [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
|
||||||
- [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
- [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
||||||
- [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
|
- [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
- [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
|
- [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
|
||||||
- [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
|
- [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
|
||||||
- [🧹 Разобрать кандидатов по тестам и записать конвенцию](items/tests-convention.md) — как пишем тесты, не записано нигде: пункт «Тесты» в convention-candidates не пересматривали, он обещает фикстуры в testdata/, которых в проекте нет — а трение накопилось (четыре внешних клиента, fakeStore с инъекцией ошибок, env-гейты, флаки-прогон, diff-coverage)
|
- [🧹 Разобрать кандидатов по тестам и записать конвенцию](items/tests-convention.md) — как пишем тесты, не записано нигде: пункт «Тесты» в convention-candidates не пересматривали, он обещает фикстуры в testdata/, которых в проекте нет — а трение накопилось (четыре внешних клиента, fakeStore с инъекцией ошибок, env-гейты, флаки-прогон, diff-coverage)
|
||||||
|
- [🐞 Не держать общий замок воркера во время обращения к qBittorrent при удалении](items/delete-holds-worker-lock.md) — на время удаления встаёт весь фон: опрос, сверка и команды остальных транспортов ждут замок до 30 с на загрузку, пачкой — минутами; замерено на ревью bulk-delete-page
|
||||||
- [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
- [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
||||||
- [🔬 Завершение загрузки через webhook](items/completion-webhook.md) — завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
|
- [🔬 Завершение загрузки через webhook](items/completion-webhook.md) — завершение сейчас ловим поллингом qBittorrent — webhook реагировал бы быстрее, но связывает нас с его конфигом (решим по опыту эксплуатации)
|
||||||
- [🔬 Кандидаты в конвенции кода](items/convention-candidates.md) — накоплен список кандидатов (внешние клиенты, конкурентность, тесты, CLI, время) — надо решить, что из них стало реальным трением, а что выдумано вперёд
|
- [🔬 Кандидаты в конвенции кода](items/convention-candidates.md) — накоплен список кандидатов (внешние клиенты, конкурентность, тесты, CLI, время) — надо решить, что из них стало реальным трением, а что выдумано вперёд
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# ✨ Удалять выбранные загрузки с файлами на отдельной странице
|
|
||||||
|
|
||||||
- **Тип:** feature
|
|
||||||
- **Категория:** Ядро продукта
|
|
||||||
- **Зачем:** удаление с файлами живёт только в danger-секции страницы одной загрузки: чтобы снести десять раздач, надо десять раз пройти путь список → карточка → подтверждение
|
|
||||||
- **Теги:** goal:bulk-download-management
|
|
||||||
|
|
||||||
Отдельная страница веб-UI, доступная из шапки: список загрузок, у которых
|
|
||||||
удаление разрешено (`done`, `orphaned`, `target_missing` —
|
|
||||||
`internal/worker/review.go:631`), чекбокс у каждой строки и одна кнопка
|
|
||||||
«Удалить выбранные». Кнопка ведёт на подтверждение, где выбранные раздачи
|
|
||||||
названы поимённо, и только оттуда уходит удаление.
|
|
||||||
|
|
||||||
Страница отдельная, чтобы не вешать режим выбора на основной список: там
|
|
||||||
карточки живые, самополлятся и перерисовываются, а выбор пользователя такое
|
|
||||||
перерисовывание переживать не обязан.
|
|
||||||
|
|
||||||
Речь только о полном удалении (снять хардлинки + снести раздачу с файлами из
|
|
||||||
qBittorrent). Групповой `Dismiss` (смена статуса без файлов) в эту задачу не
|
|
||||||
входит.
|
|
||||||
|
|
||||||
## Затрагивает
|
|
||||||
|
|
||||||
- новая страница веб-UI и ссылка на неё в `web/templates/partials/header.html`;
|
|
||||||
новый шаблон страницы и её партиалы;
|
|
||||||
- новый POST-эндпоинт группового удаления в `internal/httpapi` (рядом с
|
|
||||||
`POST /ui/downloads/{id}/delete`, `internal/httpapi/httpapi.go:143`);
|
|
||||||
- `Reviewer.Delete` воркера (`internal/worker/review.go:619`) — зовётся по
|
|
||||||
каждой выбранной загрузке;
|
|
||||||
- qBittorrent `torrents/delete` с `deleteFiles=true` — внешний сервис,
|
|
||||||
необратимая операция;
|
|
||||||
- спека `web-ui`, требования «Страницы веб-UI» и «Действия соответствуют
|
|
||||||
состоянию»;
|
|
||||||
- спека `state-reconciliation` в части `Delete` — групповой вызов не меняет
|
|
||||||
условий поштучного, но подтверждение человека теперь одно на пачку.
|
|
||||||
|
|
||||||
## Критерии приёмки
|
|
||||||
|
|
||||||
- Страница открывается из шапки и показывает только те загрузки, для которых
|
|
||||||
удаление разрешено поштучно (оракул: тест `internal/httpapi` — подставной
|
|
||||||
читатель отдаёт задачи во всех состояниях, в разметке строки есть у
|
|
||||||
`done`/`orphaned`/`target_missing` и нет у остальных).
|
|
||||||
- Удаление уходит только после явного подтверждения, и подтверждение называет
|
|
||||||
каждую выбранную раздачу поимённо (оракул: тест — POST без признака
|
|
||||||
подтверждения отвечает отказом и не делает ни одного вызова `Delete` у
|
|
||||||
подставного воркера; ответ подтверждения содержит заголовки всех выбранных).
|
|
||||||
- Отказ на одной загрузке не отменяет остальных, а результат называет
|
|
||||||
удалённые и отказавшие поимённо с причиной (оракул: тест, где второй `Delete`
|
|
||||||
возвращает ошибку — первая и третья удалены, страница результата называет
|
|
||||||
вторую и её причину).
|
|
||||||
- Групповой путь не расширяет прав поштучного: попытка удалить задачу в
|
|
||||||
состоянии, где кнопка недоступна, отклоняется с тем же отказом
|
|
||||||
(оракул: тест — `Delete` для `downloading` возвращает `ErrConflict`, страница
|
|
||||||
показывает отказ, остальные выбранные не затронуты).
|
|
||||||
- Поведение страницы записано дельта-спекой и проходит валидацию (оракул:
|
|
||||||
`openspec validate --strict` и `task gate`).
|
|
||||||
|
|
||||||
## Рамки
|
|
||||||
|
|
||||||
Необратимое действие: `deleteFiles=true` сносит файлы раздачи, гард последней
|
|
||||||
копии в `Delete` выключен сознательно. Ослаблять подтверждение ради удобства
|
|
||||||
пачки нельзя — оно остаётся обязательным и поимённым. Групповой `Dismiss` и
|
|
||||||
автоматическая чистка по сроку хранения (`db-retention-cleanup`) — не эта
|
|
||||||
задача.
|
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 🐞 Не держать общий замок воркера во время обращения к qBittorrent при удалении
|
||||||
|
|
||||||
|
- **Тип:** fix
|
||||||
|
- **Категория:** Инфраструктура
|
||||||
|
- **Зачем:** на время удаления встаёт весь фон: опрос, сверка и команды остальных транспортов ждут замок до 30 с на загрузку, пачкой — минутами; замерено на ревью bulk-delete-page
|
||||||
|
|
||||||
|
`Worker.Delete` берёт `w.mu` и держит его через сетевой вызов `qbt.Delete`, хотя
|
||||||
|
правило пакета обратное и записано рядом: медленные вызовы идут **вне** замка
|
||||||
|
(`internal/worker/worker.go`, `Poll` зовёт `qbt.Torrents` до `Lock`). Замок при
|
||||||
|
этом один на весь воркер, а не на задачу — комментарий пакета и
|
||||||
|
`docs/architecture.md` называют его per-download, и это тоже неверно.
|
||||||
|
|
||||||
|
## Воспроизведение
|
||||||
|
|
||||||
|
1. Заставить qBittorrent отвечать на `torrents/delete` медленно (подставной
|
||||||
|
клиент с задержкой; в жизни — снос раздачи с большим числом файлов на
|
||||||
|
загруженном диске).
|
||||||
|
2. Во время вызова дёрнуть через тот же воркер любую команду по **другой**
|
||||||
|
загрузке.
|
||||||
|
|
||||||
|
Видно вместо ожидаемого: команда ждёт весь чужой сетевой вызов. Замер ревью: при
|
||||||
|
задержке `qbt.Delete` 300 мс конкурентный `Cancel` по неизвестному
|
||||||
|
идентификатору заблокирован на `280.450631ms`. Таймаут клиента qBittorrent —
|
||||||
|
зашитые 30 с (поля в `[qbittorrent]` нет), поэтому верхняя граница на одну
|
||||||
|
загрузку — 30 с, а на пачку группового удаления — минуты, в течение которых не
|
||||||
|
идут ни опрос, ни сверка, ни уведомления.
|
||||||
|
|
||||||
|
Отчёт ревью с оракулами —
|
||||||
|
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`,
|
||||||
|
находка 3.
|
||||||
|
|
||||||
|
## Затрагивает
|
||||||
|
|
||||||
|
- `Worker.Delete` (`internal/worker/review.go`) — границы критической секции;
|
||||||
|
- `w.mu` и комментарий пакета `internal/worker` — «per-download» неверно;
|
||||||
|
- `docs/architecture.md`, строка «Переходы состояний» в единых точках — тот же
|
||||||
|
неверный факт;
|
||||||
|
- потолок времени на проход группового удаления (`httpapi.bulkBudget`) и
|
||||||
|
требование о нём в спеке `web-ui` — при сужении замка они теряют основание;
|
||||||
|
- поле таймаута в секции `[qbittorrent]` конфига, которого сегодня нет.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- Команда по одной загрузке не ждёт сетевого вызова по другой (оракул: тест
|
||||||
|
`internal/worker` с подставным клиентом, блокирующимся внутри `Delete`, —
|
||||||
|
`w.mu.TryLock()` из теста возвращает `true`).
|
||||||
|
- Состояние после сетевого вызова перечитывается, и удаление не переходит в
|
||||||
|
`deleted`, если задача успела уйти из допустимого состояния (оракул: тест на
|
||||||
|
смену состояния между шагами).
|
||||||
|
- Факт про замок исправлен в комментарии пакета и в `docs/architecture.md`
|
||||||
|
(оракул: `task gate`, шаг `canon`).
|
||||||
|
- Решено и записано, остаётся ли потолок времени на проход группового удаления
|
||||||
|
(оракул: дельта-спека и `openspec validate --strict`).
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Трогает операцию, которая сносит файлы: разбор идемпотентности при повторе
|
||||||
|
обязателен. Уборка собственного торрента после отмены (`processCatched`) — уже
|
||||||
|
устроена нужным образом и служит образцом, менять её не надо.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# 🐞 Не оставлять задачу в done, когда ссылки сняты, а раздача не снесена
|
||||||
|
|
||||||
|
- **Тип:** fix
|
||||||
|
- **Категория:** Ядро продукта
|
||||||
|
- **Зачем:** при недоступном qBittorrent задача весь простой соседа показывает done, хотя тайтла в Jellyfin уже нет: сверка падает на первом шаге и до коррекции не доходит
|
||||||
|
- **Теги:** goal:state-integrity
|
||||||
|
|
||||||
|
`Delete` снимает библиотечные ссылки раньше, чем зовёт `qbt.Delete`. Если сосед
|
||||||
|
недоступен, локальный шаг проходит, внешний падает, и задача остаётся в `done`.
|
||||||
|
Спека рассчитывает, что расхождение живёт «до тика сверки», но `Poll`
|
||||||
|
возвращается на первой же ошибке `qbt.Torrents` и до `reconcileDesync` не
|
||||||
|
доходит вовсе — значит, расхождение живёт весь простой qBittorrent.
|
||||||
|
|
||||||
|
Двигает у цели `state-integrity` ту же строку завершения — «ни один известный
|
||||||
|
сегодня путь не оставляет состояние, которое не объясняется историей
|
||||||
|
переходов»: `done` держится сколь угодно долго и историей переходов не
|
||||||
|
объясняется.
|
||||||
|
|
||||||
|
## Воспроизведение
|
||||||
|
|
||||||
|
1. Довести задачу до `done` с существующими библиотечными ссылками.
|
||||||
|
2. Погасить qBittorrent (в тесте — клиент на несуществующий адрес).
|
||||||
|
3. Выполнить удаление — поштучно или пачкой.
|
||||||
|
|
||||||
|
Видно вместо ожидаемого: ссылки сняты, строки `file_link` удалены, раздача цела,
|
||||||
|
место не освобождено, а карточка, список и Telegram показывают `done`. Пока
|
||||||
|
qBittorrent лежит, ни один тик сверки этого не исправит. Замер ревью: отказ
|
||||||
|
приходит за `524.545µs`, то есть пачка успевает пройти по всем выбранным раньше,
|
||||||
|
чем человек поймёт, что сосед недоступен.
|
||||||
|
|
||||||
|
Отчёт ревью —
|
||||||
|
`openspec/changes/archive/2026-08-10-bulk-delete-page/review/report.md`,
|
||||||
|
находка 4.
|
||||||
|
|
||||||
|
## Затрагивает
|
||||||
|
|
||||||
|
- `Worker.Delete` (`internal/worker/review.go`) — порядок шагов и переход после
|
||||||
|
отказа внешнего шага;
|
||||||
|
- `Worker.Poll` (`internal/worker/worker.go`) — ранний возврат по отказу
|
||||||
|
`qbt.Torrents` перед `reconcileDesync`;
|
||||||
|
- спека `state-reconciliation`, требование «Полное удаление загрузки
|
||||||
|
пользователем» — формулировка «кратковременное рассогласование до тика сверки
|
||||||
|
ожидаемо» сегодня неверна;
|
||||||
|
- граф переходов: `done → target_missing` уже легален, нового состояния не надо.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- После отказа внешнего шага при снятых ссылках задача не остаётся в `done`
|
||||||
|
(оракул: тест `internal/worker` — клиент qBittorrent отказывает, задача
|
||||||
|
оказывается в `target_missing`, а не в `done`).
|
||||||
|
- В `deleted` при этом задача не переходит: место не освобождено (оракул: тот же
|
||||||
|
тест).
|
||||||
|
- Повторное удаление после подъёма соседа идемпотентно дожимает раздачу (оракул:
|
||||||
|
тест — второй вызов при живом клиенте уводит задачу в `deleted`).
|
||||||
|
- Формулировка спеки приведена к тому, что код делает (оракул:
|
||||||
|
`openspec validate --strict` и `task gate`).
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Порядок шагов менять можно, но снос раздачи раньше снятия ссылок — отдельное
|
||||||
|
решение с ценой: оно убирает необратимый локальный шаг при мёртвом соседе, но
|
||||||
|
меняет поведение при отказе на втором шаге. Ранний возврат `Poll` по отказу
|
||||||
|
`qbt.Torrents` трогать осторожно: на нём стоит вся сверка.
|
||||||
Reference in New Issue
Block a user