Дозакрыты находки ревью по слиянию сущностей

- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из null больше не затирают маршрут. Победитель внутри доставки стал
  функцией множества версий — общим помощником с точками, — а провенанс
  поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину
  к прежнему содержимому.
- Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной
  записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма
  считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает
  на старте; текст ошибки разбора не несёт значений из тела.
- Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты:
  безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя
  был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
av
2026-08-02 16:38:18 +03:00
parent 51a5272c96
commit 8331328134
52 changed files with 4921 additions and 481 deletions
+84
View File
@@ -702,3 +702,87 @@ func TestParseНепокрытыеСекцииГраницыИДетермини
}
})
}
// Инвариант «тела запросов только на DEBUG и с обрезкой» обходился одним `%v`:
// строка в 8 МиБ на месте ожидаемого объекта давала текст ошибки в 8 МиБ, и он
// уезжал атрибутом `error` на уровень WARN — то есть содержимое доставки
// оказывалось в логе целиком и без обрезки.
//
// Предел держит само сообщение, а не обрезка на стороне логирующего: обрезка
// живёт в другом месте и о новой ошибке разбора не узнает.
func TestParseОшибкаНеНесётЗначенийИзТела(t *testing.T) {
t.Parallel()
const secret = "СЕКРЕТНОЕ-ЗНАЧЕНИЕ-ИЗ-ТЕЛА"
cases := map[string]string{
"строка вместо data": `{"data":"` + secret + strings.Repeat("A", 1<<20) + `"}`,
"строка вместо тела": `"` + secret + strings.Repeat("A", 1<<20) + `"`,
"число вместо имени секции": `{"data":{"metrics":[]},"` + secret + `":1}`,
"число вместо содержимого": `{"data":{"metrics":` + strings.Repeat("9", 1<<20) + `}}`,
}
for name, body := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()
_, err := hae.Parse([]byte(body), hae.Meta{})
if err == nil {
t.Skip("вход разобрался — проверять нечего")
}
msg := err.Error()
if len(msg) > 512 {
t.Errorf("текст ошибки %d Б: длина зависит от длины значения во входе", len(msg))
}
if strings.Contains(msg, secret) {
t.Errorf("значение из тела доехало до сообщения: %s", msg)
}
})
}
}
// Смещение обязано указывать на место ПЕРЕД виновным токеном, а не за ним.
// Декодер сообщает позицию как конец последнего возвращённого токена, поэтому
// снятая ПОСЛЕ чтения она отличалась бы от начала проблемы ровно на длину
// значения — на восемь мегабайт в том самом случае, ради которого требование и
// написано. Проверяется само свойство: смещение не зависит от длины значения.
func TestParseОшибкаНазываетТипТокенаИНачало(t *testing.T) {
t.Parallel()
short := `{"data":"` + strings.Repeat("A", 16) + `"}`
long := `{"data":"` + strings.Repeat("A", 1<<20) + `"}`
msgs := make([]string, 0, 2)
for _, body := range []string{short, long} {
_, err := hae.Parse([]byte(body), hae.Meta{})
if err == nil {
t.Fatal("ожидалась ошибка")
}
msgs = append(msgs, err.Error())
}
if !strings.Contains(msgs[0], "string") {
t.Errorf("тип токена не назван словарём JSON: %s", msgs[0])
}
// `{"data"` — семь байт: разбор виновного значения начинается здесь.
if !strings.Contains(msgs[0], "смещении 7") {
t.Errorf("смещение не указывает на место перед токеном: %s", msgs[0])
}
if msgs[0] != msgs[1] {
t.Errorf("смещение поехало вместе с длиной значения:\n %s\n %s", msgs[0], msgs[1])
}
}
// Ошибка может прийти не только от нашего разбора, но и из encoding/json, а его
// UnmarshalTypeError кладёт в текст ЛИТЕРАЛ значения: тело из миллиона цифр
// давало текст ошибки в мегабайт. Предел держится на границе пакета.
func TestParseЧужаяПричинаОбрезается(t *testing.T) {
t.Parallel()
body := `{"data":{"metrics":` + strings.Repeat("9", 1<<20) + `}}`
_, err := hae.Parse([]byte(body), hae.Meta{})
if err == nil {
t.Fatal("ожидалась ошибка")
}
if len(err.Error()) > 512 {
t.Errorf("текст ошибки %d Б: литерал из тела доехал до сообщения", len(err.Error()))
}
}