отработаны находки ревью кода

Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера,
4 «сейчас», 2 развилки. Все закрыты регрессионными тестами.

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

Четыре развилки вынесены блокерами в беклог.
This commit is contained in:
av
2026-08-01 19:02:05 +03:00
parent 32f21044e4
commit cd7a4c1493
23 changed files with 1340 additions and 175 deletions
+41 -1
View File
@@ -179,6 +179,46 @@ func TestIngestRejectsOversizedBody(t *testing.T) {
}
}
// Граница обязана стоять на РАСПАКОВАННОМ теле, а не только на сжатом.
//
// Измерено на прежней версии: 400 КиБ сжатого тела разворачивались в 400 МиБ,
// принимались со статусом 200 и выедали гигабайт. При потолке степени сжатия
// gzip около 1030:1 и штатных 64 МиБ речь о десятках гигабайт на запрос, а
// параллельные складываются. Цена отказа здесь наивысшая в проекте: приём —
// единственное место, где поток существует, и доставка, не попавшая в архив,
// не попадает в журнал вовсе.
func TestIngestRejectsGzipBomb(t *testing.T) {
h, st := newAPI(t, nil)
// Лимит в тесте — 1 МиБ (см. newAPI). Тело валидное, просто очень длинное
// и сжимается почти нацело.
payload := `{"data":{"metrics":[]},"pad":"` + strings.Repeat(" ", 8<<20) + `"}`
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
if _, err := gz.Write([]byte(payload)); err != nil {
t.Fatalf("подготовка gzip: %v", err)
}
if err := gz.Close(); err != nil {
t.Fatalf("подготовка gzip: %v", err)
}
if buf.Len() >= 1<<20 {
t.Fatalf("сжатое тело %d байт — уже больше лимита, тест проверяет не то", buf.Len())
}
req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest", &buf)
req.Header.Set("Content-Encoding", "gzip")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("статус = %d, ожидался 413: распакованное тело больше лимита", rec.Code)
}
if n, _ := st.CountDeliveries(t.Context()); n != 0 { //nolint:errcheck // проверяем только счётчик
t.Errorf("доставок = %d, ожидалось 0", n)
}
}
func TestIngestChecksTokenWhenConfigured(t *testing.T) {
h, _ := newAPI(t, []string{"верный-токен"})
@@ -236,7 +276,7 @@ func newAPI(t *testing.T, writeTokens []string) (http.Handler, *store.Store) {
log := slog.New(slog.DiscardHandler)
h := httpapi.New(httpapi.Options{
Ingest: ingest.New(arch, st, fold.New(arch, st, log), log),
Ingest: ingest.New(arch, st, fold.New(arch, st, 0, log), log),
Log: log,
WriteTokens: writeTokens,
MaxBodyMB: 1,