Жизненный цикл: claim-токен распознавания, индексация хешей, retry сломанного торрента
Три мелких фикса из docs/backlog/review-lifecycle-minor.md (ревью Fable 2026-07-08). MINOR-9 (I/O под глобальным w.mu) осознанно waive для one-user home-сервера — не трогаем. MINOR-8: claim-токен распознавания. recognizeOne фиксирует updated_at на момент claim (перечитывая запись после перехода в recognizing), а finishRecognition коммитит результат, только если токен совпал. Иначе за время LLM-вызова задачу увели из recognizing и вернули обратно (cancel → relink revive) — это уже другой эпизод, устаревший результат отбрасываем, задача остаётся в recognizing для перезапуска поллингом. NIT-11: lookup-мапы (byHash/live/torrentByInfohash) больше не индексируют усечённый 40-hex t.Hash v2-only торрентов. Новый хелпер torrentIndexHashes зеркалит выбор torrentHashes: t.Hash берём только при отсутствии обоих infohash_v1/v2. Убирает теоретический ложный матч по коллизии длины. NIT-12: retry живого, но сломанного торрента (error/missingFiles) теперь отклоняется с подсказкой починить раздачу (recheck) в qBittorrent, вместо бессмысленной переотдачи источника (сверка тут же вернула бы задачу в failed). Повторный Add — только когда раздачи в qBittorrent нет. Меняет спеку state-reconciliation → дельта openspec/changes/2026-07-17-retry-reject-broken-torrent (не архивировал). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -315,6 +315,10 @@ type memStore struct {
|
||||
// Инъекция сбоев (для тестов устойчивости раскладки).
|
||||
failCreateLinks error // CreateFileLinks вернёт эту ошибку
|
||||
failSetState func(store.State) error // SetDownloadState вернёт ошибку для перехода
|
||||
|
||||
// clock — монотонный счётчик для updated_at (как реальный store бьёт метку на
|
||||
// каждом переходе); питает проверку claim-токена распознавания (MINOR-8).
|
||||
clock int
|
||||
}
|
||||
|
||||
func newMemStore() *memStore {
|
||||
@@ -450,6 +454,8 @@ func (m *memStore) SetDownloadState(_ context.Context, id string, st store.State
|
||||
d.State = st
|
||||
d.ErrorCode = store.NullString(code)
|
||||
d.ErrorMsg = store.NullString(msg)
|
||||
m.clock++
|
||||
d.UpdatedAt = itoa(m.clock) // свежая метка перехода — claim-токен (MINOR-8)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -810,6 +816,35 @@ func TestRecognizeOne_DiscardsWhenStateChanged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRecognizeOne_DiscardsStaleClaim — MINOR-8: если во время вызова LLM задачу
|
||||
// увели из recognizing и вернули обратно (cancel → relink revive), это уже другой
|
||||
// recognizing-эпизод (updated_at сменился). Устаревший результат прежнего прогона
|
||||
// отбрасывается, задача остаётся в recognizing для перезапуска поллингом.
|
||||
func TestRecognizeOne_DiscardsStaleClaim(t *testing.T) {
|
||||
st := newMemStore()
|
||||
st.put(completedDownload("1"))
|
||||
qb := &fakeQbt{
|
||||
torrents: []qbt.Torrent{{Hash: ihTest, Name: "Show", SavePath: "/d"}},
|
||||
files: []qbt.File{{Name: "Show/e1.mkv", Size: 100}},
|
||||
}
|
||||
// Во время вызова LLM задачу уводят из recognizing и возвращают обратно
|
||||
// (симуляция cancel → relink): состояние снова recognizing, но updated_at сменился.
|
||||
rec := &fakeRecognizer{result: seriesResult(), onCall: func() {
|
||||
_ = st.SetDownloadState(context.Background(), "1", store.StateCancelled, "", "")
|
||||
_ = st.SetDownloadState(context.Background(), "1", store.StateRecognizing, "", "")
|
||||
}}
|
||||
w := testWorkerWith(st, qb, rec, nil)
|
||||
|
||||
w.recognizeOne(context.Background(), "1")
|
||||
|
||||
if st.downloads["1"].State != store.StateRecognizing {
|
||||
t.Errorf("state = %q, want recognizing (устаревший результат отброшен, ждём перезапуска)", st.downloads["1"].State)
|
||||
}
|
||||
if cur, _ := st.GetCurrentRecognition(context.Background(), "1"); cur != nil {
|
||||
t.Error("recognition устаревшей попытки не должна сохраняться")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecognizeOne_SignalsErrorToReview(t *testing.T) {
|
||||
st := newMemStore()
|
||||
st.put(completedDownload("1"))
|
||||
|
||||
Reference in New Issue
Block a user