- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
73 lines
2.7 KiB
Go
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("провенанс не обновился — тест проверяет не то")
|
|
}
|
|
}
|