Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели (разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий. - Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице «источник × цель» → состояния target_missing/orphaned/deleted, переходы и самовосстановление (healing). - worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс пропажи источника (порог [worker].source_missing_threshold) и синхронный preflight перед действиями (relink/recognize/apply/undo) — не доверяем state в БД. - layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника), отказ всего батча без частичного отката (ErrLastCopy). - store: единый список terminalStates для IsTerminal и FindActiveByInfohash (иначе семантика «активности» разъезжается), столбец source_miss_count, миграция 0003. - httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне. - Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config. Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,31 @@ func TestFindActiveByInfohash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Состояния рассинхрона (target_missing/orphaned/deleted) терминальны: задача
|
||||
// в них не должна считаться «активной» (иначе relink/ingest-дедуп решат, что
|
||||
// для infohash уже есть активная задача). Регрессия: FindActiveByInfohash и
|
||||
// IsTerminal обязаны опираться на один список терминальных состояний.
|
||||
func TestFindActiveByInfohash_DesyncStatesNotActive(t *testing.T) {
|
||||
for _, st := range []State{StateTargetMissing, StateOrphaned, StateDeleted} {
|
||||
t.Run(string(st), func(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
ih := "33333333333333333333333333333333333333" + string(st[0:2])
|
||||
|
||||
id, err := store.CreateDownload(ctx, newDownloading(ih))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.SetDownloadState(ctx, id, st, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d, err := store.FindActiveByInfohash(ctx, ih); err != nil || d != nil {
|
||||
t.Fatalf("%s: активной задачи быть не должно, получили (%v,%v)", st, d, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Терминальное состояние снимает ключ идемпотентности и позволяет завести
|
||||
// тот же infohash заново (повторная закачка спустя время).
|
||||
func TestTerminalReleasesInfohash(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user