Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки. Все закрыты регрессионными тестами. - схема точки сна определяется по самой точке, а не по индексу в исходном массиве: одна пропущенная точка меняла эпизод и сводку местами - доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (восемь прогонов — семь состояний) - граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо max_body_mb - столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта; payload без HTML-экранирования - выравнивание по местной метке: получасовые зоны уводили часовую выгрузку в minute - доставка из одних суточных сводок больше не отвергается целиком - единицы не переписываются молча; счётчик считает сохранённые точки - все выходы Fold логируются, исход пишется на переживающем отмену контексте Четыре развилки вынесены блокерами в беклог.
116 lines
4.3 KiB
Go
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())
|
|
}
|
|
}
|