непокрытые секции доставки видны в статусе разбора

- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор
  числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind,
  которых в экспорте Apple нет
- разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются
  декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ
- статус partial и колонка delivery.uncovered_sections; миграция переводит
  прежние parsed в pending — им верить нельзя
- витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
av
2026-08-01 21:25:59 +03:00
parent 7a7594e3e7
commit 34e5109b6d
26 changed files with 1808 additions and 89 deletions
+96
View File
@@ -5,6 +5,7 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -228,3 +229,98 @@ func mustHour(t *testing.T, st *store.Store, metric, layer string) time.Time {
}
return hours[0]
}
// Доставка с непокрытой секцией обязана быть ОТЛИЧИМА от разобранной целиком.
// Без этого ретеншен, ориентируясь на статус, срежет тело — а для stateOfMind
// это необратимо: в экспорте Apple секции нет, доставки HAE единственный
// источник.
func TestFoldЧастичныйРазборВиденВУчёте(t *testing.T) {
t.Parallel()
f, arch, st := newFold(t)
ctx := context.Background()
deliver(t, arch, st, "d1", "Minutes", "a1", fixture(t, "uncovered_sections.json"))
stats, err := f.Fold(ctx, "d1")
if err != nil {
t.Fatalf("свёртка: %v", err)
}
if len(stats.Uncovered) == 0 {
t.Error("список непокрытых секций пуст")
}
d, err := st.LastDelivery(ctx)
if err != nil {
t.Fatalf("чтение доставки: %v", err)
}
if d.ParseStatus != store.ParsePartial {
t.Errorf("статус %q, ожидался %q", d.ParseStatus, store.ParsePartial)
}
if !strings.Contains(d.UncoveredSections, "stateOfMind") {
t.Errorf("список в базе %q не содержит stateOfMind", d.UncoveredSections)
}
// Точки метрик обязаны сохраниться: частичность не отменяет разобранного.
if stats.Points == 0 {
t.Error("точек нет — покрытая секция потерялась вместе с непокрытой")
}
}
// Доставка из одних метрик списка не получает и остаётся parsed: частичность
// производна от списка, а не назначена.
func TestFoldПолныйРазборОстаётсяParsed(t *testing.T) {
t.Parallel()
f, arch, st := newFold(t)
ctx := context.Background()
deliver(t, arch, st, "d1", "Minutes", "a1", fixture(t, "minute.json"))
if _, err := f.Fold(ctx, "d1"); err != nil {
t.Fatalf("свёртка: %v", err)
}
d, err := st.LastDelivery(ctx)
if err != nil {
t.Fatalf("чтение доставки: %v", err)
}
if d.ParseStatus != store.ParseDone {
t.Errorf("статус %q, ожидался %q", d.ParseStatus, store.ParseDone)
}
if d.UncoveredSections != "[]" {
t.Errorf("список %q, ожидался `[]` — ровно одно представление пустоты", d.UncoveredSections)
}
}
// Список замещает прежнее значение ЦЕЛИКОМ, включая замещение пустым. Иначе
// доставка, чья секция стала покрытой, осталась бы partial навсегда, и
// ретеншен вечно щадил бы тело, которое уже не нужно.
func TestFoldПересвёрткаОчищаетСписок(t *testing.T) {
t.Parallel()
f, arch, st := newFold(t)
ctx := context.Background()
// Сперва доставка с непокрытой секцией.
deliver(t, arch, st, "d1", "Minutes", "a1", fixture(t, "uncovered_sections.json"))
if _, err := f.Fold(ctx, "d1"); err != nil {
t.Fatalf("свёртка: %v", err)
}
// Затем — та же доставка, но тело уже без непокрытых секций: так выглядит
// пересвёртка после того, как секцию научились разбирать.
deliver(t, arch, st, "d2", "Minutes", "a1", fixture(t, "minute.json"))
if _, err := f.Fold(ctx, "d2"); err != nil {
t.Fatalf("свёртка: %v", err)
}
d, err := st.LastDelivery(ctx)
if err != nil {
t.Fatalf("чтение доставки: %v", err)
}
if d.UncoveredSections != "[]" {
t.Errorf("список %q не очистился — пустой список обязан замещать прежний", d.UncoveredSections)
}
if d.ParseStatus != store.ParseDone {
t.Errorf("статус %q, ожидался %q", d.ParseStatus, store.ParseDone)
}
}