diff --git a/platform/docs/DEFECT_REGISTER.md b/platform/docs/DEFECT_REGISTER.md index dec4dc49..4c1baeb1 100644 --- a/platform/docs/DEFECT_REGISTER.md +++ b/platform/docs/DEFECT_REGISTER.md @@ -79,7 +79,7 @@ | ID | Класс | Серьёзность | Где | Суть | Статус | Источник | |---|---|---|---|---|---|---| | PD-465 | bug | info | `internal/pgstore/runs.go` (греп `ProofSpawns`), `internal/runs/spawn.go` (греп `RecordSpawn` и `Runner.Start`) | **ТРЕТИЙ СПОСОБ УЕХАТЬ ПОД ДОКАЗАТЕЛЬСТВОМ `run abandon` НЕ ЛОВИТСЯ: заявка на спавн коммитится ДО подъёма юнита.** Арбитр, построенный паком «деньги и правда на экране», сверяет id живой попытки и монотонный счётчик заявок под книжной блокировкой и закрывает два способа из трёх. Третий: `RecordSpawn` пишет `unit_name` и инкрементирует `spawns` одним оператором, а `Runner.Start` идёт СЛЕДУЮЩЕЙ строкой — доказательство, снятое между ними, читает счётчик УЖЕ увеличенным и слышит от systemd «юнита нет», потому что юнит ещё не создан; обе сверки сходятся, и списание попадает на попытку, чей юнит вот-вот поднимется. ⚠ Окно — миллисекунды между двумя операторами одной функции, и достижимо только если оператор снимает доказательство ровно в них. Цена та же, что у всякого неверного доказательства: живой движок тратит против своего книжного потолка, холд аккаунта закрыт, провайдеру платит деплой (ограничено потолком этого прогона, аккаунтом не эксплуатируемо). ⛔ **Лечение требует НОВОГО состояния — «заявка в полёте», которого сегодня не пишет никто**, поэтому пак его не строил: заморозка скоупа, и добавление способности — не закрытие находки. Кандидаты: писать `spawns` ПОСЛЕ подъёма юнита (меняет смысл счётчика на «успешно поднятых»), либо отдельная отметка, снимаемая на `Start`/`unclaim`. Найдено приёмкой оркестратора №23 по коду | open | приёмка пака «деньги и правда на экране», 07.09 | -| PD-464 | bug | minor | `internal/books/parse.go` (греп `const UploadSettle`), бутовый гейт `internal/config/config.go` (греп `UploadSettle`) | ⛔ **КОНСТАНТА ОБЪЯВЛЯЕТ ИНВАРИАНТ, КОТОРОГО НЕ ДЕРЖИТ, И ТРИ ВЫВОДА ЕЁ ЗНАЧЕНИЯ РУКАМИ ПОДРЯД БЫЛИ НЕВЕРНЫ.** `UploadSettle` обещает покрыть всё, что загрузка делает ПОСЛЕ тела, и бутовый гейт на этом обещании допускает дедлайн. Замер 07.09 (двоичный поиск по `Load()`, хвост выведен ПО КОДУ пути): объявлено **3m30s**, реальный худший хвост **3m40s** (`StartParsing` 30 + разрез 90 + `FinishParse` 30 + `ReleaseParseClaim` 30 + `ReadBook` 30 + квитанция 10), недобор **10 с**. Гейт принимает дедлайн до **26m29s**, ложь начинается с **26m21s** — окно шириной восемь секунд, достижимое только ручной настройкой почти вплотную к потолку гейта; на дефолте `10m` запас **16m20s**. ⚠ **Наблюдаемое следствие — ДУБЛЬ КНИГИ, а не потеря:** претензия на ключ идемпотентности, пережившая `ClaimStale`, перехватывается повтором с новым токеном (`internal/pgstore/idempotency.go`, ветка сравнения возраста претензии с `ClaimStale`), и человек, повторивший «висящую» загрузку, получает две книги вместо реплея одной. ⛔ **ЛЕЧЕНИЕ — НЕ ПОДНЯТЬ ЧИСЛО:** оно выводилось руками трижды и трижды было неверным, каждый раз по новой причине (пропущен шаг · неверный бюджет квитанции · два последовательных шага записаны как альтернативы), поэтому четвёртый вывод руками — подпорка, а не починка (`D39.216`). Границу надо выводить ИЗ кода пути. ⚠ Достижимость худшего пути БЕЗ искусственного замедления **не измерена**: она требует конъюнкции «большая книга» и «три полных `writeBudget` подряд», а гейт живого движка на хосте замера не закрыт. Первый из семи пунктов, с которыми синхронный разрез уезжает отдельным паком ⚠ **ДИСПОЗИЦИЯ ПАКА «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ» (08.09, в дереве, статус флипает лендинг): ЗАКРЫТО ОБЕИМИ ПОЛОВИНАМИ.** (1) Значение приведено к замеру: квитанция стала ТЕРМИНОМ суммы, `UploadSettle = CutBudget + 4*writeBudget + ReceiptBudget` = **220 с** — ровно тот худший хвост, что намерен слева; носитель у величины квитанции теперь ОДИН (`books.ReceiptBudget`, тратит её `httpapi.settleCtx`), и разойтись им нечем. (2) ⭐ И это НЕ то, что закрывает строку, потому что четвёртый вывод суммы руками был бы четвёртой заплатой: хвост целиком идёт под ОДНИМ отсоединённым дедлайном (`books.walk`), каждый шаг берёт `min(свой бюджет, остаток хвоста)` (`books.step`), и добавленный завтра шаг границу НЕ ДВИГАЕТ ПО ПОСТРОЕНИЮ — сумма теперь отвечает лишь за то, чтобы в обычном худшем случае ни один шаг не был урезан. Предъявлено: `TestNoStepOfAnUploadsTailOutlivesTheWalk` (в т.ч. 50 вложенных шагов), `TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget`, `TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget`, `TestTheReceiptSpendsTheShareTheIntakeSetAsideForIt`; посадки M1/M2/M3/M7 красные с топичным текстом. ⚠ Попутно закрыт СОСЕДНИЙ промах того же гейта, которого строка не называла: он сверял дедлайн с `min(UploadGrace, ClaimStale)` и пропускал `ClaimGrace` — свип мог забрать claim у книги, чья загрузка ещё идёт (п.7 десятки). Окон в гейте теперь ТРИ. | fixed | пак «деньги и правда на экране», круг 5 самопроверки, 07.09 | +| PD-464 | bug | minor | `internal/books/parse.go` (греп `const UploadSettle`), бутовый гейт `internal/config/config.go` (греп `UploadSettle`) | ⛔ **КОНСТАНТА ОБЪЯВЛЯЕТ ИНВАРИАНТ, КОТОРОГО НЕ ДЕРЖИТ, И ТРИ ВЫВОДА ЕЁ ЗНАЧЕНИЯ РУКАМИ ПОДРЯД БЫЛИ НЕВЕРНЫ.** `UploadSettle` обещает покрыть всё, что загрузка делает ПОСЛЕ тела, и бутовый гейт на этом обещании допускает дедлайн. Замер 07.09 (двоичный поиск по `Load()`, хвост выведен ПО КОДУ пути): объявлено **3m30s**, реальный худший хвост **3m40s** (`StartParsing` 30 + разрез 90 + `FinishParse` 30 + `ReleaseParseClaim` 30 + `ReadBook` 30 + квитанция 10), недобор **10 с**. Гейт принимает дедлайн до **26m29s**, ложь начинается с **26m21s** — окно шириной восемь секунд, достижимое только ручной настройкой почти вплотную к потолку гейта; на дефолте `10m` запас **16m20s**. ⚠ **Наблюдаемое следствие — ДУБЛЬ КНИГИ, а не потеря:** претензия на ключ идемпотентности, пережившая `ClaimStale`, перехватывается повтором с новым токеном (`internal/pgstore/idempotency.go`, ветка сравнения возраста претензии с `ClaimStale`), и человек, повторивший «висящую» загрузку, получает две книги вместо реплея одной. ⛔ **ЛЕЧЕНИЕ — НЕ ПОДНЯТЬ ЧИСЛО:** оно выводилось руками трижды и трижды было неверным, каждый раз по новой причине (пропущен шаг · неверный бюджет квитанции · два последовательных шага записаны как альтернативы), поэтому четвёртый вывод руками — подпорка, а не починка (`D39.216`). Границу надо выводить ИЗ кода пути. ⚠ Достижимость худшего пути БЕЗ искусственного замедления **не измерена**: она требует конъюнкции «большая книга» и «три полных `writeBudget` подряд», а гейт живого движка на хосте замера не закрыт. Первый из семи пунктов, с которыми синхронный разрез уезжает отдельным паком ⚠ **ДИСПОЗИЦИЯ ПАКА «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ» (08.09, ЗАЛАНДЁН `ddcbf0c`, акт `D39.229`): ЗАКРЫТО ОБЕИМИ ПОЛОВИНАМИ.** (1) Значение приведено к замеру: квитанция стала ТЕРМИНОМ суммы, `UploadSettle = CutBudget + 4*writeBudget + ReceiptBudget` = **220 с** — ровно тот худший хвост, что намерен слева; носитель у величины квитанции теперь ОДИН (`books.ReceiptBudget`, тратит её `httpapi.settleCtx`), и разойтись им нечем. (2) ⭐ И это НЕ то, что закрывает строку, потому что четвёртый вывод суммы руками был бы четвёртой заплатой: хвост целиком идёт под ОДНИМ отсоединённым дедлайном (`books.walk`), каждый шаг берёт `min(свой бюджет, остаток хвоста)` (`books.step`), и добавленный завтра шаг границу НЕ ДВИГАЕТ ПО ПОСТРОЕНИЮ — сумма теперь отвечает лишь за то, чтобы в обычном худшем случае ни один шаг не был урезан. Предъявлено: `TestNoStepOfAnUploadsTailOutlivesTheWalk` (в т.ч. 50 вложенных шагов), `TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget`, `TestTheWalkLeavesTheReceiptItsShareOfTheSettleBudget`, `TestTheReceiptSpendsTheShareTheIntakeSetAsideForIt`; посадки M1/M2/M3/M7 красные с топичным текстом. ⚠ Попутно закрыт СОСЕДНИЙ промах того же гейта, которого строка не называла: он сверял дедлайн с `min(UploadGrace, ClaimStale)` и пропускал `ClaimGrace` — свип мог забрать claim у книги, чья загрузка ещё идёт (п.7 десятки). Окон в гейте теперь ТРИ. | fixed | пак «деньги и правда на экране», круг 5 самопроверки, 07.09 | | PD-463 | bug | minor | `internal/books/parse.go` (греп `ChaptersTotal < 2 && atIntake`), контракт `RejectReason` | **ВХОД СУЖЕН ДО ТОГО, ЧТО УМЕЕТ ВЫДАЧА, ТОЛЬКО НА СИНХРОННОЙ ВЕТВИ ПРИЁМА.** Отказ книге, которую движок нарезал в одну главу, стоит под условием `atIntake`: на ветви, куда книга уходит при деплойном классе или превышении бюджета разреза, тот же манифест доезжает до `FinishParse`, и «книга одним полотном» попадает в библиотеку молча. ⚠ **Закрыть на асинхронной ветви СЕГОДНЯ НЕЧЕМ, и это не недосмотр:** словарь `RejectReason` контракта закрыт четырьмя значениями, и ни одно не описывает «прочли, нарезали, а такую форму мы пока не отдаём» — ближайшее по словам `source_unreadable` ТЕРМИНАЛЬНО и УДАЛЯЕТ исходник, то есть уничтожает файл пользователя из-за НАШЕГО ограничения. ⇒ нужен либо пятый `RejectReason` (контрактный минор, зона оркестратора), либо решение, что асинхронная ветвь остаётся проницаемой. Достижимость узкая: ветвь берётся только когда синхронный разрез не дал вердикта. Найдено опровергателем по готовой работе | open | пак «деньги и правда на экране», 07.09 | | PD-459 | bug | info | `internal/runs/reconcile.go:327`=`const settlementBackoffCap`, пины `internal/runs/stuck_settlement_test.go:202,582` | **Потолок отсрочки расплаты НЕ ЗАПИНЕН ЗНАЧЕНИЕМ: оба теста, которые его читают, сравниваются С САМОЙ КОНСТАНТОЙ.** Замер 06.09: `grep settlementBackoffCap` даёт 6 хитов в `reconcile.go` (строки 157, 162, 172, 309, 312, 327) и 4 в тестах, и в обоих тестах он стоит по обе стороны сравнения (`wait > settlementBackoffCap`) ⇒ мутация значения константы не краснит ничего. Константа несёт продуктовое свойство — отсрочка расплаты и есть resume-гейт пользователя, и её удлинение держит человека за блокировку, с которой он ничего сделать не может (довод в самом комментарии константы). ⇒ нужен пин на ЧИСЛО, а не на границу. Найдено чтением журнала при выносе эры P9–P13 | open | зона, 06.09, триаж журнала | | PD-460 | bug | info | `cmd/tmplatformctl/seed.go:44`=`usd := fs.String("usd", "25",` | **`seed` по умолчанию начисляет $25 и на стенде с ЖИВЫМИ ключами провайдера.** Команда дев-интейка не различает стенд с заглушкой и стенд, поднятый против настоящих моделей, а $25 на таком стенде — это разрешение потратить их. Кандидат лечения, названный ещё при наблюдении за живым потоком: дефолт 0 и обязательный `--usd`. ⚠ Не срочно и не эксплуатируемо аккаунтом: команда админская и требует доступа к БД. Найдено чтением журнала при выносе эры | open | зона, 06.09, триаж журнала | diff --git a/platform/docs/platform-PROGRESS.md b/platform/docs/platform-PROGRESS.md index 8bed15c9..f5bd949d 100644 --- a/platform/docs/platform-PROGRESS.md +++ b/platform/docs/platform-PROGRESS.md @@ -7,13 +7,68 @@ | Вопрос | Ответ | |---|---| -| последний заленджённый пак | «деньги и правда на экране» (06–07.09, `fda0679`, акт **D39.221**), канон контракта `0.13.0`. ⚠ Обе величины берутся ПРИБОРОМ, а не отсюда: `git log --oneline -1 -- platform/` и `grep '^ version:' ../../docs/architecture/14-api-contract/openapi.yaml` — эта строка стареет, они нет | -| пак в дереве, не закоммиченный | «разрез приёма до готовности и правда о себе» (08.09) — отчёт ниже | +| последний заленджённый пак | «разрез приёма до готовности и правда о себе» (08.09, `ddcbf0c`, акт **D39.229**), канон контракта `0.13.0`. ⚠ Обе величины берутся ПРИБОРОМ, а не отсюда: `git log --oneline -1 -- platform/` и `grep '^ version:' ../../docs/architecture/14-api-contract/openapi.yaml` — эта строка стареет, они нет. ⭐ И стареет она БЫСТРЕЕ, чем кажется: её уже правил этот пак по §4.5, и лендинг того же пака сделал её неверной снова — читай прибором, а не глазами | +| пак в дереве, не закоммиченный | дофикс по паку «разрез приёма» (две позиции, 08.09) — отчёт ниже | | открытые дефекты | `DEFECT_REGISTER.md` (счёт — `python3 docs/scripts/counts.py` от корня) | | нормы и приёмка | `ENGINEERING_STANDARDS.md` · направление — `PLATFORM_DIRECTION.md` · стек и стенд — `STACK_DECISIONS.md` | | незакрытые куски работы | `../BACKLOG.md` (`П-N`) | | как разворачивается | `../deploy/README.md` | +## ДОФИКС ПО ПАКУ «РАЗРЕЗ ПРИЁМА» — ДВЕ ПОЗИЦИИ (08.09, `textmachine-fa`, после лендинга `ddcbf0c`) + +> Заказ оркестратора №23: он закрывал пробел в СВОЕЙ приёмке (норма требует двух верификаторов по +> сданной работе, он их не поставил и поставил задним числом), и слепой верификатор нашёл две вещи в +> моей зоне. Обе я пере-проверила прежде, чем чинить. Правило остановки объявлено заказчиком заранее: +> дофикса ровно два, дальнейшее — строка бэклога. + +**1. Рукописная тройка в резерве — класс `D39.216` ВНУТРИ пака, нанятого его убрать.** +`cutTailReserve()` возвращал `3 * s.write()`, где тройка — ручной счёт записей, следующих за разрезом, +и жила она только в докстринге. Замер подтверждён мной: **0 хитов в тестах** при 3 хитах в не-тестах. +Четвёртая пост-разрезная запись — и резерв тихо недокрывает, а последствие называет ⛔-абзац моего же +`stepLeaving`: оборванная терминальная запись, книга в `parsing` без задания до свипа. + +⇒ Выбрана вторая из двух названных форм — **тройка ПИНИТСЯ**, а не выводится. Довод против первой, +чтобы он не пропал: вывести резерв структурно значит дать прогулке список её оставшихся шагов, то есть +вернуть тот самый рукописный перечень одним уровнем ниже. Вместо этого — сверка с ДРУГИМ выражением +того же факта: `UploadSettle` уже собирает прогулку из разреза и четырёх записей, ровно одна из +которых (`StartParsing`) идёт ДО разреза, значит резерв обязан равняться остатку прогулки после выноса +разреза и этой одной записи. **Два выражения делят константы, но не маршрут** — потому это сверка, а +не тавтология. + +Предъявлено посадкой: пятая запись, добавленная в `UploadSettle` и НЕ добавленная в счёт, красит +`TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt` — и он **ЕДИНСТВЕННЫЙ красный** на трёх +пакетах (`books`, `config`, `httpapi`), текстом: «the cut reserves 1m30s … but the walk (4m0s) has +2m0s left once the cut and the write before it are taken out». + +**2. `PD-464` нёс ровно ту протухшую фразу, которую пак снял с трёх других рядов.** +Диспозиция говорила «(08.09, в дереве, статус флипает лендинг)» при колонке статуса `fixed` и +состоявшемся лендинге. Замер: рядов с этой фразой в файле был **ровно один — мой собственный**. +Приведена к лендингу (`ЗАЛАНДЁН ddcbf0c, акт D39.229`); теперь фразы в файле **0** при 465 рядах, +колонок ≠ 7 — **0**, `counts.py --check` — **EXIT=0**. + +⭐ **Класс, из-за которого это уцелело, стоит назвать: норма §4.5 сработала на ТРЁХ чужих рядах и не +сработала на ОДНОМ моём.** Ряд я писала сама и потому не подпала под собственную правку. Тот же класс +оркестратор поймал у себя часом раньше на строке 253. Общее правило: **проход по норме обязан включать +строки, которые этот же проход и создал** — иначе он чистит только унаследованное. + +**Числа дофикса, сняты после последней правки кода:** `make check` со всеми четырьмя гейтами — +**MAKE-EXIT=0** · пакетов `ok` **20** · строк FAIL **0** · линтер «**0 issues**» · скипов **5** (условие +прежнее и названное). Моих файлов в дереве — **4, все в `platform/`**. ⚠ `git status` показывает **7**: остальные три +(`docs/BACKLOG.md`, `docs/ORCHESTRATOR_SESSION_PROMPT.md`, `docs/architecture/05-decisions-log.md`) — +живая работа оркестратора в ЕГО зоне, идущая параллельно. Не мои и не тронуты; называю их, чтобы «все в +моей зоне» не читалось как «в дереве больше ничего нет». Якоря дофикс НЕ сдвинул: 7 проблемных на +`HEAD` и 7 у меня, новых ноль (замер дифференциальный, как в основном отчёте). +⚠ Код выхода взят на этот раз ВЕРНО и подтверждён вторым источником: в прошлый раз я написала +`echo "MAKE-EXIT=$?"` после подстановки `$(git rev-parse …)`, и `$?` ловил код `git`, а не `make` — +напечатанный ноль не значил ничего. Теперь `st=$?` стоит сразу за `make`, и рядом вердикт самой цели: +`make check` СОХРАНЯЕТ `.check.log.` при провале и удаляет при успехе, лога нет. + +**Попутно — прибрала свой мусор на общем стенде.** Убитые прогоны (в том числе мой, когда я гасила +зависший пакет) оставили в общем Postgres **34** скретч-базы ≈9,5 МБ каждая. Удалены все 34, отказов +**0**, осталось **0**. ⚠ Инструмент выбран самоохраняющийся: обычный `drop database` БЕЗ `with (force)` +— он ОТКАЖЕТ, если между листингом и дропом кто-то подключился, вместо того чтобы выдернуть базу из-под +чужого прогона. Верификатор не стал их трогать именно из-за этого риска, и это была верная осторожность. + ## ПАК «РАЗРЕЗ ПРИЁМА ДО ГОТОВНОСТИ И ПРАВДА О СЕБЕ» — ОТЧЁТ (08.09, `textmachine-fa`) > Промт `docs/PLATFORM_INTAKE_TRUTH_SESSION_PROMPT.md`, вход HEAD `3f4680c`, дерево на входе чисто. diff --git a/platform/internal/books/parse.go b/platform/internal/books/parse.go index 772a7b6f..8a555f02 100644 --- a/platform/internal/books/parse.go +++ b/platform/internal/books/parse.go @@ -499,6 +499,15 @@ func (s *Service) stepLeaving(ctx context.Context, budget, reserve time.Duration // test shortens the write budget to reach cases a thirty-second one cannot, and a reserve pinned to // the constant would then be three real writes' worth of a walk measured in milliseconds — that is, // larger than the whole walk, so no cut would ever run and the fixture would silently model nothing. +// +// ⚠ The THREE is a count of the writes that follow a cut, and a count in code is exactly what this +// intake's budgets were rewritten to stop relying on. It is kept because the alternative — deriving +// the reserve at run time — needs the walk to enumerate the steps it has left, which is the +// hand-written list all over again, one level down. What replaces the missing structure is a check +// against the OTHER expression of the same fact: UploadSettle already composes the walk out of a cut +// and four writes, so the reserve must equal what is left of the walk once the cut and the ONE write +// that precedes it (StartParsing) are taken out. A write added to that sum and not to this count +// makes the two disagree, and TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt says so. func (s *Service) cutTailReserve() time.Duration { return 3 * s.write() } // write is the budget one terminal write gets: the constant, or what a test shortened it to. diff --git a/platform/internal/books/walk_test.go b/platform/internal/books/walk_test.go index d79eb323..caf50626 100644 --- a/platform/internal/books/walk_test.go +++ b/platform/internal/books/walk_test.go @@ -152,3 +152,37 @@ func TestTheCutOfAnUploadIsBoundedByTheWalkAndNotByItsOwnBudget(t *testing.T) { granted, CutBudget) } } + +// The reserve a cut leaves behind is the same fact as UploadSettle's own composition, reached the +// other way round. +// +// ⛔ Why this exists at all: `cutTailReserve` counts the writes after a cut BY HAND (a literal three), +// and a hand count in code is what this intake's budgets were rewritten to stop relying on — the +// class that made UploadSettle short three editions running. The count cannot be replaced by a +// derivation without giving the walk a list of its remaining steps, which is the hand-written list +// again one level down. So it is CROSS-CHECKED instead: UploadSettle composes the walk out of one +// cut and four writes, exactly one of which (StartParsing) runs before the cut, so the reserve must +// be what is left after removing the cut and that one write. +// +// The two expressions share the constants and NOT the route, which is what makes this a check rather +// than a tautology: add a fifth write to UploadSettle and the derived side grows while the literal +// three does not, so this test — and only this test — turns red. +func TestTheCutsReserveIsTheWalkMinusTheCutAndTheWriteBeforeIt(t *testing.T) { + s := &Service{} + walk := UploadSettle - ReceiptBudget + want := walk - CutBudget - writeBudget + if got := s.cutTailReserve(); got != want { + t.Errorf("the cut reserves %s for the writes after it, but the walk (%s) has %s left once the cut (%s) "+ + "and the write before it (%s) are taken out.\n"+ + "The two disagree, which means UploadSettle and cutTailReserve no longer count the same steps: "+ + "a write was added to one of them and not to the other. Whichever grew, the reserve is now wrong — "+ + "too small truncates the write that records what the cut found, too large starves the cut itself.", + got, walk, want, CutBudget, writeBudget) + } + // And the shortened form a test uses stays proportional, or every short-walk fixture silently + // models a walk with no room for a cut at all (the hang this pack's own repair introduced). + short := &Service{writeBudget: 25 * time.Millisecond} + if got, want := short.cutTailReserve(), 3*25*time.Millisecond; got != want { + t.Errorf("with a shortened write budget the reserve is %s, want %s", got, want) + } +}