Files
healthlog/internal/store/entity_internal_test.go
T
av 8331328134 Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из null больше не затирают маршрут. Победитель внутри доставки стал
  функцией множества версий — общим помощником с точками, — а провенанс
  поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину
  к прежнему содержимому.
- Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной
  записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма
  считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает
  на старте; текст ошибки разбора не несёт значений из тела.
- Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты:
  безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя
  был квадратичен по числу присланных версий одного ключа.
2026-08-02 16:38:18 +03:00

73 lines
2.7 KiB
Go

package store
import (
"context"
"encoding/json"
"path/filepath"
"testing"
"time"
)
// Внутренний тест, потому что проверяемое наружу не отдаётся: `updated_at` —
// колонка, а не поле модели. Обещание «метка означает изменение содержимого, а
// не касание строки» держится только этой проверкой, и внешний тест для неё
// потребовал бы публичного метода ради теста.
func TestMergeПовторНеДвигаетМеткуИзменения(t *testing.T) {
t.Parallel()
ctx := context.Background()
st, err := Open(filepath.Join(t.TempDir(), "healthlog.db"))
if err != nil {
t.Fatalf("открытие базы: %v", err)
}
t.Cleanup(func() { _ = st.Close() })
raw := json.RawMessage(`{"id":"w1","route":[{"lat":1},{"lat":2}],"totalEnergy":{"qty":20}}`)
wo := IncomingEntity{
ID: "w1",
Kind: "workouts",
Start: time.Date(2025, 6, 5, 7, 0, 0, 0, time.UTC),
End: time.Date(2025, 6, 5, 7, 10, 0, 0, time.UTC),
Raw: raw,
}
merge := func(id string, at time.Time) MergeStats {
t.Helper()
s, err := st.Merge(ctx, Incoming{Workouts: []IncomingEntity{wo}},
DeliveryRef{ID: id, ReceivedAt: at})
if err != nil {
t.Fatalf("слияние: %v", err)
}
return s
}
updatedAt := func() string {
t.Helper()
var v string
if err := st.db.QueryRowContext(ctx,
`SELECT updated_at FROM workout WHERE id = 'w1'`).Scan(&v); err != nil {
t.Fatalf("чтение updated_at: %v", err)
}
return v
}
base := time.Date(2025, 6, 5, 8, 0, 0, 0, time.UTC)
merge("d1", base)
before := updatedAt()
// Тренировка приезжает до 26 раз, пока источник её досчитывает. Провенанс
// при этом обязан подняться, а метка изменения — нет: иначе она становится
// меткой касания строки, и запрос «что изменилось с момента X» получает 26
// ложных изменений, неотличимых от настоящего досчёта.
merge("d2", base.Add(5*time.Minute))
if after := updatedAt(); after != before {
t.Errorf("метка изменения двинулась без изменения содержимого: %s → %s", before, after)
}
w, err := st.Workout(ctx, "w1")
if err != nil {
t.Fatalf("чтение тренировки: %v", err)
}
if w.Delivery != "d2" {
t.Fatal("провенанс не обновился — тест проверяет не то")
}
}