Land the repair of two store tests that went green without checking anything, and ratify the norm that a test may not assert about time when time is not its subject

This commit is contained in:
heaven 2026-08-29 15:37:08 +03:00
parent 355d204e99
commit 18636ac03f
6 changed files with 262 additions and 43 deletions

View file

@ -1,7 +1,9 @@
package store
import (
"bufio"
"fmt"
"io"
"math"
"math/rand"
"os"
@ -12,16 +14,51 @@ import (
"time"
)
// kill9_test.go — Р6/Phase 0 acceptance test: kill -9 mid-work loses
// at most one in-flight call and NEVER desyncs the money from the
// checkpoints. A real SIGKILL against a subprocess (the standard library's
// helper-process pattern), not emulation.
// kill9_test.go — Р6/Phase 0 acceptance test: kill -9 mid-work loses at most one in-flight call and
// NEVER desyncs the money from the checkpoints. A real SIGKILL against a subprocess (the standard
// library's helper-process pattern), not emulation.
//
// Invariant after any kill: committed_usd == SUM(checkpoints.cost_usd)
// — because settle and the checkpoint are written in ONE transaction. Orphaned
// reserved_usd are cleared by the recovery pass on open.
// Invariant after any kill: committed_usd == SUM(checkpoints.cost_usd) — settle and the checkpoint are
// written in ONE transaction. Orphaned reserved_usd are cleared by the recovery pass on open.
//
// The helper announces its first committed checkpoint on stdout and the parent kills it only once that
// line has arrived. That handshake is what gives the assertions their subject: the kill is known to land
// on a process that has already committed, so a checkpoint the helper CONFIRMED before dying has to be
// on disk when the database is reopened. That is durability, not merely internal consistency, and it is
// the stronger of the two claims this test can make. Liveness is a fact the helper reports rather than
// an inference from a clock, so a helper that never commits is a loud failure instead of a round that
// quietly asserts nothing.
//
// The database is shared across rounds, so every stored figure is cumulative and only the INCREMENT
// speaks about the round that just ran: a total is non-zero for the rest of the run once any round has
// written, whatever the helper does afterwards. Each round therefore compares against the previous
// round's own reading and requires strict growth, which is also why no round needs an exemption.
//
// Two shapes this test deliberately does not take:
// - a line per commit would fill the 64 KB pipe and stall the helper, leaving the test measuring its
// own plumbing. The helper prints exactly once, and the parent drains to EOF regardless so nothing
// the testing framework writes later can block either.
// - polling the database while the helper runs would block on the exclusive flock store.Open takes.
// Nothing polls: the previous round's reading is this round's baseline, and no second connection is
// ever opened against a live helper.
const killLoopEnv = "TM_STORE_KILL_LOOP_DB"
const (
killLoopEnv = "TM_STORE_KILL_LOOP_DB"
// killLoopRoundEnv scopes the helper's request hashes to its round. Checkpoints are idempotent by
// request_hash (ON CONFLICT DO NOTHING), so two rounds that generated the same hash would settle
// once between them, leave the count flat, and make the increment check below report a durability
// failure that never happened. The round makes each round's writes unambiguously its own.
killLoopRoundEnv = "TM_STORE_KILL_LOOP_ROUND"
// killLoopReady is printed by the helper exactly once, after its FIRST settle+checkpoint has
// committed. It is the whole liveness signal: everything the parent asserts afterwards is about work
// that is known to be on disk.
killLoopReady = "KILL9-HELPER-COMMITTED-FIRST-CHECKPOINT"
// killLoopReadyWait bounds the wait for that signal. It is deliberately far larger than any plausible
// first-commit latency (the race detector's ~15× on this loop included): it is not a performance
// budget, it is the difference between "the helper is slow" and "the helper is not committing", and
// only the second is a failure.
killLoopReadyWait = 60 * time.Second
)
func TestHelperKillLoop(t *testing.T) {
path := os.Getenv(killLoopEnv)
@ -40,6 +77,7 @@ func TestHelperKillLoop(t *testing.T) {
if err != nil {
os.Exit(1)
}
round := os.Getenv(killLoopRoundEnv)
// Infinite reserve → settle+checkpoint loop; the parent will SIGKILL us
// at a random moment. No Release — recovery will clear the "crashed" reservations.
for i := 0; ; i++ {
@ -48,7 +86,7 @@ func TestHelperKillLoop(t *testing.T) {
os.Exit(1)
}
cp := Checkpoint{
RequestHash: fmt.Sprintf("hash-%d-%d", os.Getpid(), i),
RequestHash: fmt.Sprintf("hash-%s-%d-%d", round, os.Getpid(), i),
JobID: job.ID, ChunkIdx: i, Stage: "draft", Role: "translator",
ModelRequested: "m", ModelActual: "m",
ResponseText: "ответ", UsageJSON: "{}", CostUSD: 0.001,
@ -56,31 +94,108 @@ func TestHelperKillLoop(t *testing.T) {
if err := s.SettleWithCheckpoint(res, 0.001, cp, nil); err != nil {
os.Exit(1)
}
if i == 0 {
// ONCE, and only after the write is durable. Announcing before the settle would put the
// parent's whole assertion back on a guess.
fmt.Println(killLoopReady)
}
}
}
// killHelper is a running helper plus the one piece of bookkeeping its lifecycle needs: os/exec is
// explicit that "it is incorrect to call Wait before all reads from the pipe have completed", because
// Wait closes the pipe out from under the reader. drained is closed when the reader has reached EOF,
// and killAndReap is the only place Wait is called.
type killHelper struct {
cmd *exec.Cmd
drained <-chan struct{}
}
// killAndReap SIGKILLs the helper and collects it in the order os/exec requires.
func (h *killHelper) killAndReap(t *testing.T, round int) {
t.Helper()
if err := h.cmd.Process.Signal(syscall.SIGKILL); err != nil {
t.Fatalf("round %d: SIGKILL: %v", round, err)
}
<-h.drained // every read from the pipe has completed…
_ = h.cmd.Wait() // …and only now may Wait close it
}
// startKillHelper launches the helper and returns once it has COMMITTED its first checkpoint, so the
// caller knows the kill it is about to send will land on a process that has already done real work.
// Everything it can go wrong with is a loud failure, never a skip and never a silent pass.
func startKillHelper(t *testing.T, round int, dbPath string) *killHelper {
t.Helper()
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(exe, "-test.run", "^TestHelperKillLoop$", "-test.v")
cmd.Env = append(os.Environ(), killLoopEnv+"="+dbPath, fmt.Sprintf("%s=%d", killLoopRoundEnv, round))
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("round %d: helper stdout pipe: %v", round, err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("round %d: start helper: %v", round, err)
}
// One reader for the whole life of the process: it reports the marker and then keeps draining, so the
// pipe can never fill and stall the helper mid-loop. It reports reaching EOF without the marker as an
// error rather than as silence, because "the helper never committed" and "the helper died" are
// different failures and a test that confuses them sends the next reader down the wrong path.
ready := make(chan error, 1)
drained := make(chan struct{})
go func() {
defer close(drained)
sc := bufio.NewScanner(stdout)
seen := false
for sc.Scan() {
if !seen && sc.Text() == killLoopReady {
seen = true
ready <- nil
}
}
if !seen {
ready <- fmt.Errorf("helper exited without ever committing a checkpoint (scanner: %v)", sc.Err())
}
// An over-long line would have stopped the scanner above; keep draining regardless so a stalled
// pipe can never be mistaken for a stalled store.
_, _ = io.Copy(io.Discard, stdout)
}()
h := &killHelper{cmd: cmd, drained: drained}
select {
case err := <-ready:
if err != nil {
<-drained
_ = cmd.Wait()
t.Fatalf("round %d: %v — there was nothing for SIGKILL to land on, so everything asserted afterwards would be vacuous", round, err)
}
case <-time.After(killLoopReadyWait):
h.killAndReap(t, round)
t.Fatalf("round %d: the helper did not commit a first checkpoint within %s — the kill would have landed on a process that had done nothing, and everything asserted afterwards would be vacuous",
round, killLoopReadyWait)
}
return h
}
func TestKillMinus9LosesAtMostOneCall(t *testing.T) {
if testing.Short() {
t.Skip("subprocess test")
}
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "kill9.db")
// The database is shared across rounds, so every stored figure is cumulative. The previous round's own
// reading is this round's baseline — which is also why no second connection is ever opened while the
// helper holds the project lock.
var prevCheckpoints int
var prevCommitted float64
for round := 0; round < 5; round++ { // D23.4: 3→5 rounds — more SIGKILL windows per run
cmd := exec.Command(exe, "-test.run", "^TestHelperKillLoop$", "-test.v")
cmd.Env = append(os.Environ(), killLoopEnv+"="+path)
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
// Let it run for a random amount of time, then SIGKILL — nothing graceful.
time.Sleep(time.Duration(150+rand.Intn(400)) * time.Millisecond)
if err := cmd.Process.Signal(syscall.SIGKILL); err != nil {
t.Fatal(err)
}
_ = cmd.Wait()
helper := startKillHelper(t, round, path)
// The kill lands at a random point of a reserve→settle cycle. This delay varies WHERE it lands;
// whether there is anything for it to land on was settled by the readiness signal.
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
helper.killAndReap(t, round)
s, err := Open(path) // the recovery pass clears the stuck reservations
if err != nil {
@ -107,9 +222,19 @@ func TestKillMinus9LosesAtMostOneCall(t *testing.T) {
t.Fatalf("round %d: committed=%v != sum(checkpoints)=%v over %d checkpoints — settle+checkpoint not atomic",
round, committed, checkpointSum, checkpointCount)
}
if checkpointCount == 0 && round > 0 {
t.Fatalf("round %d: helper made no progress", round)
// DURABILITY: the helper reported a committed checkpoint before it was killed, so this round must
// have added at least one, in every round including the first.
if checkpointCount <= prevCheckpoints {
t.Fatalf("round %d: %d checkpoints before the kill, %d after reopening — a checkpoint the helper confirmed as committed did not survive SIGKILL, or the round did no work the previous rounds had not already done",
round, prevCheckpoints, checkpointCount)
}
t.Logf("round %d: %d checkpoints, committed=$%.6f — consistent after SIGKILL", round, checkpointCount, committed)
// The money must have moved with them, by the same increment logic.
if committed <= prevCommitted {
t.Fatalf("round %d: committed went from $%.6f to $%.6f across a round that added %d checkpoint(s)",
round, prevCommitted, committed, checkpointCount-prevCheckpoints)
}
t.Logf("round %d: +%d checkpoints (%d total), committed=$%.6f — a confirmed commit survived SIGKILL and the ledger matches",
round, checkpointCount-prevCheckpoints, checkpointCount, committed)
prevCheckpoints, prevCommitted = checkpointCount, committed
}
}

View file

@ -397,29 +397,60 @@ func TestTheMigrationSeamRunsUnderTheLockAndOnlyWhenAStepWillRun(t *testing.T) {
}
}
// TestTheSeamIsNotChargedToTheStoreOperationBudget: the seam's work is the CALLER's (a restore point
// whose cost scales with the book), and opTimeout bounds operations that are O(1) in the size of the
// book. While the two shared one deadline, a large project failed the migration on the copy it had just
// paid for — «context deadline exceeded» on the first step, exit 1 outside the refusal band, and a
// retry that copied the whole database again (round-2 review, reproduced on 2.6 GB).
// seamBudget is what opTimeout is shrunk to while the migration under test runs. Under -race with every
// core busy, the three budgeted operations of a head-1 open — schemaBaseline, one applyStep, the
// recovery pass — cost about 70 ms in total on this machine, so a per-operation second leaves them some
// forty times the room they need. The seam then sleeps a multiple of it, and that sleep is the only time
// this test costs the battery.
const seamBudget = 1 * time.Second
// TestTheSeamIsNotChargedToTheStoreOperationBudget: the seam's work is the CALLER's — a restore point
// whose cost scales with the book — while opTimeout bounds operations that are O(1) in the size of the
// book. Sharing one deadline between the two makes a large project fail its migration on the copy it has
// just paid for: «context deadline exceeded» on the first step, exit 1 outside the refusal band, and a
// retry that copies the whole database again (round-2 review, reproduced on 2.6 GB).
//
// The budget is shrunk instead of the seam sleeping ten real seconds: same property, no dead time in
// the battery. It is restored on the way out and nothing here runs in parallel.
// The starting point is built BEFORE the budget is shrunk. A head-1 Open is seventeen budgeted
// operations — schemaBaseline, fifteen migration steps, the recovery pass — and not one of them is the
// subject here. Under a shrunken deadline they would be real DDL running a race they have no reason to
// be in, and on a busy machine they lose it: the same load that costs the head-1 open 70 ms costs this
// fixture up to 1.10 s, an average of 65 ms per operation. The shrink covers the migration under test
// and nothing else.
//
// The seam's elapsed time is measured and required to exceed the budget because that is this test's
// PRECONDITION rather than a consequence of it. A sleep that failed to outlive the deadline would let
// the migration succeed for the ordinary reason and report a green proving nothing, so vacuity is a
// failure here. Time is read in the one direction where reading it is safe: a slow machine can only make
// the seam longer.
func TestTheSeamIsNotChargedToTheStoreOperationBudget(t *testing.T) {
head := SchemaHead()
defer func(original time.Duration) { opTimeout = original }(opTimeout)
opTimeout = 150 * time.Millisecond
path := filepath.Join(t.TempDir(), "book.db")
openAtVersion(t, path, head-1)
openAtVersion(t, path, head-1) // fixture FIRST, at the full budget: 17 operations that are not the subject
defer func(original time.Duration) { opTimeout = original }(opTimeout)
opTimeout = seamBudget
seams := 0
var seamTook time.Duration
applied, err := Migrate(path, func(Migration) error {
time.Sleep(3 * opTimeout) // a restore point on a book far larger than the budget
seams++
start := time.Now()
time.Sleep(3 * seamBudget) // a restore point on a book far larger than the budget
seamTook = time.Since(start)
return nil
})
if err != nil {
t.Fatalf("a slow seam must not fail the migration it protects: %v", err)
}
// The precondition, checked rather than assumed: without a seam that genuinely outlived the budget
// there is no claim here, and a green result would mean nothing.
if seams != 1 {
t.Fatalf("the seam ran %d time(s), want exactly 1 — with no seam this test asserts nothing", seams)
}
if seamTook <= seamBudget {
t.Fatalf("the seam took %v, which did NOT outlive the %v operation budget — the migration "+
"would have survived a shared deadline too, so this test proved nothing", seamTook, seamBudget)
}
if applied.From != head-1 || applied.To != head {
t.Fatalf("transition %+v, want v%d -> v%d", applied, head-1, head)
}

View file

@ -135,7 +135,7 @@ D-ссылка грепается по D-логу · mid-flight аддендум
| Характер | Чем проверяется |
|---|---|
| Код | Исполнением: батарея + живые пробы + СОБСТВЕННЫЕ адверсариальные посадки ВНЕ списка автора; сверка с индустриальным первоисточником (stdlib/RFC/устоявшаяся библиотека прежде велосипеда); пак, меняющий отгружаемые байты или добавляющий механизм, принимается прогоном через настоящий драйвер, не чтением диффа; у шва — инвентарь ВСЕХ каналов другой стороны чтением ЧУЖОГО кода, не по памяти (D39.132) |
| Код |**Тест утверждает не то, что думает (D39.171):** часы как способ синхронизации там, где предмет утверждения НЕ время, и ассерт по подстроке в буфере, куда пишет кто-то ещё. Обе формы дают и ложное красное, и **тихо-зелёное**а оно дороже: зелёное, не проверившее ничего, не перечитывает никто. Исполнением: батарея + живые пробы + СОБСТВЕННЫЕ адверсариальные посадки ВНЕ списка автора; сверка с индустриальным первоисточником (stdlib/RFC/устоявшаяся библиотека прежде велосипеда); пак, меняющий отгружаемые байты или добавляющий механизм, принимается прогоном через настоящий драйвер, не чтением диффа; у шва — инвентарь ВСЕХ каналов другой стороны чтением ЧУЖОГО кода, не по памяти (D39.132) |
| Полигон / замеры | Пере-выводом чисел из СЫРЬЯ; независимость сигналов + leave-one-out (сфабрикованная сходимость — типовой провал); катастроф-скрин к победителю; вывод на агрегате до вскрытия единиц запрещён (D39.61) |
| Ресёрч / текст | Claim-fidelity: цитаты — дословные подстроки первоисточников (типовой провал — ОВЕР-АТРИБУЦИЯ, не фабрикация); пере-открыть выборку улик; опровергатель ДРУГОЙ моделью |
| Доки | Сверка утверждений против кода и живых носителей |

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
# Реестр D-нот — карта актуальности v2 (D1D39.170;
# Реестр D-нот — карта актуальности v2 (D1D39.171;
> ⚠ **СЛАБОЕ МЕСТО, КОТОРОЕ БЫЛО ЗДЕСЬ (вписано 22.08, ЗАКРЫТО 24.08 — D39.157 п.6).** Колонка ТЕЛА
> у нот D39.107…D39.123 говорила «жив», хотя тела уехали в слайс подрезкой D39.139; семнадцать строк
@ -231,3 +231,4 @@
| D39.168 | 28.08 | **Аудит доков, часть 2 — архитектура и ресёрчи.** Две НОРМЫ требовали построенного (закон шва: «`status --json` версию не несёт — закрыть», а несёт); доки врали про `accepts_labels` («пусты у всех» при трёх заполненных эндпоинтах — ось аккаунта); денежный вход цитировал грант $5 при коде 0. Вынесены в `archive/research/` четыре мёртвых отчёта (290 КБ) по графу входящих ссылок; двум ресёрчам без шапки вовсе поставлены ревю-шапки. | жив | ЖИВОЕ: системный класс line-якорей в архитектуре; часть 3 не пройдена | доки ресёрчи архив аудит |
| D39.169 | 29.08 | **Платформенный пак P11 принят и заленджен + контрактный минор 0.8.0.** Отзыв сессии гасит открытый SSE-поток (`PD-379`, единственная уязвимость `major`) и объявляет причину кадром `session_ended`, а не молчаливым обрывом в `401`; застрявший расчёт видим и закрываем; денежная группа `PD-384/391/394/397/376`. Второй рубеж сессии поймал СЕМЬ ложных подтверждений в её же отчёте (включая выдуманную мутационную посадку) — код цел, достоверность рассказа нет. Охотник вне карты нашёл ЧЕТЫРЕ регресса, введённых самим паком, все закрыты до сдачи. Две мои ошибки в каноне: схема нового кадра была сиротой вне союза `anyOf`, перечень кадров соединения противоречил телу схемы. Линтер якорей нашёл 21 указатель на исчезнувший код — норма: гонять его ПОСЛЕДНИМ шагом, после того как код замер. | жив | ЖИВОЕ: `PD-425` — открытый денежный major, отсрочка на оркестраторе; `PD-424`/`PD-426` отложены с доводом; `PD-423` — условие батареи, которого не знал рецепт | платформа контракт деньги сессии приёмка |
| D39.170 | 29.08 | **Движковый пак «деньги» принят и заленджен**: потолок ОБЪЁМА оплаченной работы (`--max-units`) + читающий путь `status` теперь СВОРАЧИВАЕТ банк, поэтому смета пере-прохода впервые доезжает до покупателя ДО покупки, оставаясь $0. ⚠ Пак СНЯЛ ПОСЫЛКУ чужой зоны: платформа не берёт `rebill_*` по доводу «status читает ноль сразу после apply» (`ingest/resync.go:37-43`) — довод устарел. Четыре круга приёмки; блокирующая находка охотника: потолок ПРОБИВАЛСЯ пере-сидом банка посреди прогона (грант 1 → 4 вызова, $0.0073 вместо $0.0036, две доставленные главы оплачены дважды и названы бесплатными). Лечение структурное — пере-план + отказ волны работать с планом чужого снапшота. Сессия не воспроизвела сценарий ПЯТЬ раз при верных прогонах: дельту двигает не текст, а предложения черновиков, а `spoilerBlocked` (`membank/memory.go:640-647`) режет термин с поздним `since_ch` навсегда. | жив | ЖИВОЕ: ось отгрузки на `once_key` — отдельный заказ; терминолог вне потолка — вход в калибровку цены; проводка `--max-units` ГЕЙЧЕНА | движок деньги потолок приёмка шов |
| D39.171 | 29.08 | **Гейт денежной безопасности при `kill -9` четверть раундов проверял ТАВТОЛОГИЮ.** `kill9_test.go` сторожил живость счётчиком по ВСЕЙ базе, а путь создаётся вне цикла раундов ⇒ страж пуст в КАЖДОМ раунде; на пустой базе `committed == sum` выполняется как `0 == 0`. Замер: 10 раундов из 40 не утверждали ничего. Доказано сравнением двух форм под ОДНОЙ посадкой: старый тест PASS при четырёх пустых раундах, новый FAIL на первом. Второй тест ужимал бюджет ДО построения фикстуры (17 чужих операций под усечением). Оба починены структурно (рукопожатие + сверка прироста; усечение только на предмет), боевой код не тронут. **Норма: тест не имеет права утверждать о ВРЕМЕНИ, если предмет утверждения — не время; тихо-зелёное дороже красного.** | жив | ЖИВОЕ: две замеренные формы дефектного теста; узость класса проверяется по формам и зоне, а не вообще | тесты гейты движок приёмка |

View file

@ -1,4 +1,4 @@
# Журнал решений оркестратора — контракт D1D39.170 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
# Журнал решений оркестратора — контракт D1D39.171 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`)
> **КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Работая с контрактом (греп номера: живой файл → слайсы, целиком НЕ читать — D39.125), держи под рукой, что чем перекрыто:
> ⚠ **Эррата 09.08 (D39.125):** D39.111 п.1 предписывал промту S3 «максимум = баланс МИНУС открытые холды» — формула ОШИБОЧНА (вычитание дважды), исправлена D39.115 п.2(а): максимум = Balance КАК ЕСТЬ; тело D39.111 живёт ниже в этом файле (голова D39.106+).
@ -1332,3 +1332,65 @@ $0.007280 вместо максимум $0.003640, две уже доставл
бесполезно), именованный кап — по тому, что заплатит ЭТОТ прогон (законная работа не отклоняется),
прогон без пере-оплаты гейта не встречает. Первая редакция отозвана самой сессией и помечена отозванной,
а не переписана молча.
## D39.171 — ГЕЙТ ДЕНЕЖНОЙ БЕЗОПАСНОСТИ ПРИ `kill -9` ЧЕТВЕРТЬ РАУНДОВ ПРОВЕРЯЛ ТАВТОЛОГИЮ: два дефектных теста движка починены, норма «тихо-зелёное дороже красного» ратифицирована (29.08, оркестратор №19). ✅
**Что заленджено.** Два теста `backend/internal/store/`; **боевой код не тронут ни строкой** — показаний против движка нет ни у кого, падавшие раунды печатали согласованность, проверка резерваций не срабатывала ни разу.
⚠ **ГЛАВНОЕ: дефект был не в том, что тест иногда краснел, а в том, что он зеленел, не проверив ничего.**
`kill9_test.go` сторожил живость условием `checkpointCount == 0 && round > 0` по счётчику ВСЕЙ базы,
а путь к базе создаётся ОДИН раз вне цикла раундов. Значит после первого коммита счётчик уже никогда
не ноль — и страж пуст в КАЖДОМ раунде, а не только в нулевом, как предполагал заказ. Замер сессии:
8 прогонов × 5 раундов под `-race` с восемью busy-loop — **10 раундов из 40 не утверждали ничего**
(нулевой во всех восьми прогонах плюс два с нулевым приростом, прошедшие молча). Денежный инвариант
`committed == sum(checkpoints)` на пустой базе выполняется тавтологически как `0 == 0`.
**ДОКАЗАНО СРАВНЕНИЕМ ДВУХ ФОРМ ПОД ОДНОЙ ПОСАДКОЙ (моя проверка, не отчёт сессии).** Посадил хелпера,
который со второго раунда не делает ни одного коммита. **Старый тест: `PASS`**, печатая «26 checkpoints»
в раундах 04 — четыре раунда не сделали ничего, и он отрапортовал «consistent after SIGKILL».
**Новый: `FAIL` на первом же**, с диагнозом «a checkpoint the helper confirmed as committed did not
survive SIGKILL, or the round did no work the previous rounds had not already done».
**Форма лечения — структурная, по входному условию владельца «не хакать, а рефакторить».** Снятие
`&& round > 0` и подъём таймаута отвергнуты как лечение симптома. Построено рукопожатие: хелпер печатает
ОДНУ строку после первого успешного `SettleWithCheckpoint`, родитель ждёт её с громким падением по
таймауту, и только затем шлёт `SIGKILL`; сверяется ПРИРОСТ за раунд. `round > 0` исчез не как снятая
проверка, а как артефакт кумулятивного счётчика. ⚠ **Тест стал СТРОЖЕ:** коммит подтверждён ДО смерти
процесса, поэтому прирост после переоткрытия — проверка ДОЛГОВЕЧНОСТИ, которой старая форма не делала
вовсе. Две ловушки реализации обойдены явно: строка на каждый коммит забила бы недренируемый пайп на
64 КБ, а `Open()` из родителя взял бы эксклюзивный `flock` и заблокировался.
**Второй тест: дефект оказался НЕ в утверждении, а в фикстуре.** `TestTheSeamIsNotChargedToThe`
`StoreOperationBudget` ужимал `opTimeout` до 150 мс ПЕРВОЙ строкой, до `openAtVersion`, — а открытие
head-1 это семнадцать бюджетируемых операций, ни одна из которых не предмет теста. Под `-race` с
нагрузкой фикстура стоит до 1.10 с. Подпись подтверждает: старый падал на `migration 14`, шаге
ФИКСТУРЫ, при том что предмет — шаг 16. Починка: фикстура строится под полным бюджетом, усечение
накрывает только миграцию под тестом (три операции вместо двадцати). ⚠ И тест приобрёл то, чего не
утверждал: его ПРЕДПОСЫЛКА (шов реально пережил бюджет) бралась из константы сна — теперь время шва
меряется и обязано превысить бюджет, то есть вакуумная зелень стала падением, а медленная машина может
проверку только усилить. Мой прогон после починки: **8 из 8 под `-race` с восемью busy-loop**.
**Сессия нашла в СВОЁМ коде две вещи адверсариальным проходом, обе настоящие:** `cmd.Wait()` закрывал
трубу, пока горутина ещё читала (os/exec прямо называет это некорректным) — введён `killAndReap`,
ждущий EOF; и `request_hash` хелпера строился из PID, так что совпадение PID между раундами сделало бы
`settle` пустышкой через `ON CONFLICT DO NOTHING` — и её же проверка прироста соврала бы про потерю
долговечности. Хеш скоуплен раундом.
**РАТИФИЦИРОВАННАЯ НОРМА (общая, за пределами этих тестов).** **Тест не имеет права утверждать что-либо
о времени, если предмет утверждения — не время.** В обоих случаях предмет — атомарность и учёт, а часы
попали туда как способ синхронизации и стали слабым местом. Две ЗАМЕРЕННЫЕ формы такого теста:
(1) часы как синхронизация — дают и ложное красное, и **тихо-зелёное**, когда подготовка не успела;
(2) ассерт, ищущий значение ПОДСТРОКОЙ в буфере, куда пишет кто-то ещё — признак дешёвый: сверка идёт
с текстом, а не с полем (живой случай — `355d204`, где наносекундный таймстемп сталкивался с искомой
суммой). ⚠ **Тихо-зелёное дороже красного:** красное раздражает и потому чинится, а зелёное, не
проверившее ничего, не перечитывает никто.
**Узость класса проверяется ПО ФОРМАМ и ПО ЗОНЕ, которые реально обошли.** Замер сессии по
`backend/**`: семь `time.Sleep` в тестах, ровно один ужатый таймаут, ровно одна конструкция «ассерт под
условием, которое при провале подготовки не выполняется» — то есть оба случая и есть почти весь класс.
Но «форм X в зоне Y почти нет» ≠ «обвязка здорова»: вторая форма приехала в дерево из ДРУГОЙ зоны в тот
же день. Сессия сузила собственный вердикт сама, до того как я успел записать его шире.
**Цена расхождения замеров названа:** мои первые прогоны шли БЕЗ `-race`, и 2764 чекпоинта против 2668
под флагом — разница инструмента, а не хоста. Вывод «не воспроизвёл» был получен не тем инструментом, и
это тот самый третий исход, который нельзя сваливать во второй.