Быстрый приём: сохранение в catched, добавление в qBittorrent — шаг worker'а
Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` — ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent вынесены в асинхронный шаг машины состояний, который двигает worker. - store: состояние `catched` (нетерминальное, активная группа); атомарный переход PromoteCatched (catched → downloading + display_name) с гардом state='catched' (ре-валидация после сетевых вызовов вне блокировки) - ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу - worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий переход; сбой add оставляет catched (ретрай тиком); предохранитель catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи - config: worker.catch_timeout (дефолт 10m) - веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг карточки/страницы до перехода в downloading; Telegram-текст без сырого catched - OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change заархивирован Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,64 @@ func newDownloading() *Download {
|
||||
}
|
||||
}
|
||||
|
||||
func newCatched() *Download {
|
||||
return &Download{
|
||||
SourceType: SourceMagnet,
|
||||
SourceRef: "magnet:?xt=urn:btih:test",
|
||||
Context: "ctx",
|
||||
State: StateCatched,
|
||||
}
|
||||
}
|
||||
|
||||
// catched — нетерминальное активное состояние: его наличие блокирует повторный
|
||||
// приём того же infohash (инвариант «≤1 активная на infohash»).
|
||||
func TestCatchedIsActiveForDedup(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
const ih = "aabbccddeeff00112233445566778899aabbccdd"
|
||||
|
||||
d1 := newCatched()
|
||||
if existing, err := st.CreateDownloadIfNoActive(ctx, d1, []string{ih}); err != nil || existing != nil {
|
||||
t.Fatalf("первый catched: existing=%v err=%v", existing, err)
|
||||
}
|
||||
// Повторный приём того же хеша → дедуп на активную catched-задачу.
|
||||
existing, err := st.CreateDownloadIfNoActive(ctx, newCatched(), []string{ih})
|
||||
if err != nil {
|
||||
t.Fatalf("повторный приём: %v", err)
|
||||
}
|
||||
if existing == nil || existing.ID != d1.ID {
|
||||
t.Errorf("ожидался дедуп на catched %s, got %v", d1.ID, existing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromoteCatched(t *testing.T) {
|
||||
st := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
const ih = "aabbccddeeff00112233445566778899aabbccdd"
|
||||
|
||||
d := newCatched()
|
||||
if _, err := st.CreateDownloadIfNoActive(ctx, d, []string{ih}); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if err := st.PromoteCatched(ctx, d.ID, "Дюна (2024)"); err != nil {
|
||||
t.Fatalf("promote: %v", err)
|
||||
}
|
||||
got, err := st.GetDownload(ctx, d.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if got.State != StateDownloading {
|
||||
t.Errorf("state = %q, want downloading", got.State)
|
||||
}
|
||||
if got.DisplayName != "Дюна (2024)" {
|
||||
t.Errorf("display_name = %q", got.DisplayName)
|
||||
}
|
||||
// Повторный promote (уже не catched) — отклоняется гардом state='catched'.
|
||||
if err := st.PromoteCatched(ctx, d.ID, "X"); err == nil {
|
||||
t.Error("ожидалась ошибка promote для не-catched задачи")
|
||||
}
|
||||
}
|
||||
|
||||
// mustCreate заводит загрузку с хешем и возвращает её id; дедуп на
|
||||
// существующую активную — ошибка теста.
|
||||
func mustCreate(t *testing.T, st *Store, infohash string) string {
|
||||
|
||||
Reference in New Issue
Block a user