web-ui: карточка и страница обновляются, пока задачу может двигать фон

- условие самообновления — доменный предикат store.State.IsObservable() вместо
  фазы catched; один поллер на поверхность, интервалы 5 с и 15 с
- отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели
  вместо 404/500, который htmx не свопит
- заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел
  «Живой поллинг» в конвенции веб-UI
This commit is contained in:
av
2026-08-10 14:02:38 +03:00
parent 969926fae3
commit a5d873b62d
29 changed files with 1575 additions and 99 deletions
+30
View File
@@ -653,3 +653,33 @@ func TestListAndByState(t *testing.T) {
t.Fatalf("ListDownloadsByState(downloading) = %v", dl)
}
}
// TestIsObservable: наблюдаемость — «состояние ещё может измениться без
// человека». Нетерминальные наблюдаемы все; из терминальных — те, которые фон
// возвращает в поток сам (failed по восстановимым кодам, target_missing и
// orphaned переоценивает сверка). done в перечень не входит сознательно: его
// переоценка означает удаление файлов мимо сервиса.
func TestIsObservable(t *testing.T) {
observable := []State{
StateCatched, StateDownloading, StateCompleted, StateRecognizing,
StateReview, StateLinking, StateDeferred, StateStuck,
StateFailed, StateTargetMissing, StateOrphaned,
}
silent := []State{StateDone, StateCancelled, StateReverted, StateDeleted}
for _, s := range observable {
if !s.IsObservable() {
t.Errorf("%s: IsObservable=false, want true", s)
}
}
for _, s := range silent {
if s.IsObservable() {
t.Errorf("%s: IsObservable=true, want false", s)
}
}
// Наблюдаемое множество не сводится к нетерминальному — иначе предикат был
// бы лишним, а карточка упавшей задачи замирала бы навсегда.
if !StateFailed.IsTerminal() || !StateFailed.IsObservable() {
t.Error("failed должно быть терминальным и при этом наблюдаемым")
}
}