Жизненный цикл: Defer запрещён из пре-источникового catched (MAJOR-6)
Команда 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>
This commit is contained in:
@@ -550,6 +550,12 @@ func (w *Worker) Defer(ctx context.Context, id string) (err error) {
|
||||
if d.State.IsTerminal() {
|
||||
return fmt.Errorf("defer: download %s is terminal (%s): %w", id, d.State, ErrConflict)
|
||||
}
|
||||
// Пре-источниковое catched (торрент ещё не добавлен в qBittorrent) откладывать
|
||||
// нечего: задача не дошла до ревью, а catched → deferred увёл бы её в лимбо —
|
||||
// processCatched листает только catched и больше её не подхватит (MAJOR-6).
|
||||
if d.State == store.StateCatched {
|
||||
return fmt.Errorf("defer: источник ещё не добавлен в qBittorrent (%s), отложить можно после добавления: %w", d.State, ErrConflict)
|
||||
}
|
||||
ctx = w.scoped(ctx, capReview, id, d.PrimaryInfohash())
|
||||
w.transition(ctx, *d, store.StateDeferred, "", "")
|
||||
return nil
|
||||
|
||||
@@ -944,6 +944,25 @@ func TestDefer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Defer из пре-источникового catched отклоняется конфликтом: торрент ещё не
|
||||
// добавлен в qBittorrent, откладывать нечего, а catched → deferred увёл бы
|
||||
// задачу в лимбо → необратимый deleted (MAJOR-6). Состояние не меняется.
|
||||
func TestDeferRejectsCatched(t *testing.T) {
|
||||
st := newMemStore()
|
||||
d := completedDownload("1")
|
||||
d.State = store.StateCatched
|
||||
st.put(d)
|
||||
w := testWorkerWith(st, &fakeQbt{}, &fakeRecognizer{}, nil)
|
||||
|
||||
err := w.Defer(context.Background(), "1")
|
||||
if !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("Defer from catched = %v, want ErrConflict", err)
|
||||
}
|
||||
if st.downloads["1"].State != store.StateCatched {
|
||||
t.Errorf("state = %q, want catched (не тронуто)", st.downloads["1"].State)
|
||||
}
|
||||
}
|
||||
|
||||
// applyFixture — реальный layouter с temp-библиотеками и исходными файлами.
|
||||
type applyFixture struct {
|
||||
w *Worker
|
||||
|
||||
Reference in New Issue
Block a user