Files
healthlog/internal/fold/log_test.go
T
av cd7a4c1493 отработаны находки ревью кода
Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера,
4 «сейчас», 2 развилки. Все закрыты регрессионными тестами.

- схема точки сна определяется по самой точке, а не по индексу в исходном
  массиве: одна пропущенная точка меняла эпизод и сводку местами
- доставка сворачивается одной транзакцией: частичное состояние было
  недетерминированным (восемь прогонов — семь состояний)
- граница размера на распакованном теле: 400 КиБ gzip разворачивались в
  400 МиБ мимо max_body_mb
- столкновение — расхождение канонических форм, а не байтов; WARN с
  координатами объекта; payload без HTML-экранирования
- выравнивание по местной метке: получасовые зоны уводили часовую выгрузку
  в minute
- доставка из одних суточных сводок больше не отвергается целиком
- единицы не переписываются молча; счётчик считает сохранённые точки
- все выходы Fold логируются, исход пишется на переживающем отмену контексте

Четыре развилки вынесены блокерами в беклог.
2026-08-01 19:02:05 +03:00

116 lines
4.3 KiB
Go

package fold_test
import (
"bytes"
"context"
"encoding/json"
"log/slog"
"path/filepath"
"strings"
"testing"
"git.vakhrushev.me/av/healthlog/internal/archive"
"git.vakhrushev.me/av/healthlog/internal/fold"
"git.vakhrushev.me/av/healthlog/internal/store"
)
// Данные о здоровье чувствительнее токенов, и требование «значения точек не в
// логах» до сих пор не проверялось ничем: все тестовые логгеры выбрасывали
// записи. Тест ловит записи и смотрит на них.
func TestFoldНеПишетЗначенийВЛог(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
dir := t.TempDir()
arch, err := archive.New(filepath.Join(dir, "raw"))
if err != nil {
t.Fatalf("архив: %v", err)
}
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
if err != nil {
t.Fatalf("база: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
f := fold.New(arch, st, 0, log)
// Значения и имя устройства выбраны так, чтобы их нельзя было спутать ни с
// чем: если они окажутся в логе, это будет видно.
body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` +
`{"date":"2025-06-05 10:00:00 +0300","qty":424242.7,"source":"Секретные Часы Антона"},` +
`{"date":"2025-06-05 10:01:00 +0300","qty":313131.9,"source":"Секретные Часы Антона"},` +
`{"date":"2025-06-05 10:02:00 +0300","qty":151515.1,"source":"Секретные Часы Антона"}` +
`]}]}}`)
deliver(t, arch, st, "d1", "Minutes", "auto-1", body)
if _, err := f.Fold(context.Background(), "d1"); err != nil {
t.Fatalf("свёртка: %v", err)
}
logged := buf.String()
if logged == "" {
t.Fatal("свёртка не записала ни одной строки — чекпоинт молчит")
}
for _, secret := range []string{"424242", "313131", "151515", "Секретные Часы"} {
if strings.Contains(logged, secret) {
t.Errorf("в логе оказалось %q:\n%s", secret, logged)
}
}
// И одновременно — чекпоинт обязан нести счётчики, иначе он бесполезен.
var rec map[string]any
for line := range strings.SplitSeq(strings.TrimSpace(logged), "\n") {
if err := json.Unmarshal([]byte(line), &rec); err != nil {
t.Fatalf("строка лога не JSON: %v", err)
}
if rec["msg"] == "delivery folded" {
break
}
}
for _, attr := range []string{"delivery_id", "points", "stored", "buckets", "layer", "skipped"} {
if _, ok := rec[attr]; !ok {
t.Errorf("в записи нет атрибута %q: %v", attr, rec)
}
}
}
// Доставка, у которой отброшены ВСЕ точки, — это сломавшийся формат, а не
// штатная работа. Без WARN смена формата метки выглядела бы как здоровый
// поток: 200, parsed, INFO, points=0.
func TestFoldВсеТочкиОтброшеныДаётWarn(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
dir := t.TempDir()
arch, err := archive.New(filepath.Join(dir, "raw"))
if err != nil {
t.Fatalf("архив: %v", err)
}
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
if err != nil {
t.Fatalf("база: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
f := fold.New(arch, st, 0, log)
// Метки в формате, которого разбор не знает: HAE сменил формат.
body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` +
`{"date":"2025-06-05T10:00:00Z","qty":1},{"date":"2025-06-05T10:01:00Z","qty":2}` +
`]}]}}`)
deliver(t, arch, st, "d1", "Minutes", "auto-1", body)
if _, err := f.Fold(context.Background(), "d1"); err != nil {
t.Fatalf("свёртка: %v", err)
}
if !strings.Contains(buf.String(), `"level":"WARN"`) {
t.Errorf("доставка без единой сохранённой точки записана не как WARN:\n%s", buf.String())
}
}