watcher_sustained_failure_logs_once_issue2015 failed on macos-14 CI with
`failing_index_calls == 13, expected 14`. Its loop ran exactly 14
touch+poll rounds and assumed each one reached the index callback. A poll
whose git probe fails transiently (spawn refused under runner load) is a
fail-closed no-op by design (#937): the baseline stays uncommitted and the
next poll retries. So one refused spawn costs one attempt, and the count
ends at 13. This is the same test race as watcher_unwatch_drains_pending_free,
not a production defect. Holding one exhausted spawn mid-loop with
cbm_subprocess_force_spawn_eagain_for_testing reproduces the CI line
exactly.
The loop now polls until the 14th failed attempt instead of counting
polls. Each poll runs at most one attempt for the single project, so it
stops at exactly 14. A watcher that never reindexes hangs, and the runner
reports that as a failure, never a pass. The log-once contract means the
same thing as before: watcher.index.sustained_failure is emitted only on a
failed index attempt when the streak equals the threshold, so extra no-op
polls cannot add lines. An `==` to `>=` mutation in watcher.c still turns
it red (`sustained_log_hits == 5, expected 1`).
The change is now a dirty worktree instead of a commit. If the baseline
poll itself failed, the old commit would be adopted as the baseline HEAD
and never reindexed, so the wait would never end. The baseline keeps the
dirty signature at "clean known", so a dirty change is seen whenever the
baseline lands. With spawns forced to fail at the baseline poll and again
mid-loop, the test passes after 16 polls for 14 attempts.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>