Files
sencho/e2e
Anso c92004dc6a fix(blueprints): retry a refused drift repair instead of stranding the row (#2028)
* fix(blueprints): retry a refused drift repair instead of stranding the row

A drift repair that could not finish left the deployment row reading
`correcting` forever, so nothing ever retried it. `drift_enforce_start`
writes `correcting` before the repair runs, and a repair that then answered
without ever reaching a deploy wrote no status of its own. That state is
in-flight as far as the decision is concerned, so `computeDecision` skipped it
and the preview projection rendered it as the informational
`in_flight_correct`. No later tick re-checked the node: the workload kept
running the moved image, one correction-failed alert fired, and then nothing,
for as long as the drift lasted. Four ticks on a refused repair recorded one
drift check, one repair attempt and one alert, and did nothing on the rest.

A refusal, or a repair that throws, now settles the row back to `drifted` with
the reason in `last_error`, so the next tick checks and retries it. A throw
counts because resolving a remote node's platform label is a network call made
before any deploy, so it can reject with nothing written and used to escape to
the per-blueprint catch with the row still `correcting`.

The row deliberately does not become `failed` or `pending`. Both are retry
buckets for an ordinary deploy, so the next tick would repair by tag and adopt
whatever the tag points at now, which is the one thing a digest-pinned repair
exists to prevent.

The failure alert fires when the row changed across the attempt, compared
against a snapshot read before the repair ran. It cannot be decided from the
row afterwards: a repair that reaches a deploy writes its own `last_error` and
returns the very same string, so asking the row afterwards finds the failure
already sitting there and mistakes a new incident for a repeat, dropping the
first failure of every kind silently. Losing the deploy lock is a deferral
rather than a failure, so the row still leaves `correcting` with a reason and
the operator is not paged about a race they cannot act on. A converged row now
forgets its last failure, as the deploy acknowledgement already did, so a
failure that returns after a repair counts as a new incident.

computeDecision also treats a `drifted` row at the current revision as a
drift-check target. The projection already reached it through its own status
fallback with the same action, severity and confirmable membership, so nothing
an operator sees changes; it is here so the decision a tick acts on is not two
files away in the preview projection.

Verified: 10333 backend tests pass with no failures, typecheck and lint clean,
and an e2e case drives drift, repair and reconvergence on a real Inline
Blueprint, asserting the row returns to Active with no drift summary and the
workload runs the approved digest again.

* fix(blueprints): correct the drift repair settle found in review

Four corrections to the refused-repair settle, all found by reviewing the
change rather than by a failing test.

The deferral note was the serious one. Losing the deploy lock is silent, and
it wrote a note about the race into `last_error`, which is the replay key. One
lost-lock tick between two identical failures therefore displaced the failure
being replayed, and the next tick reported that same known failure again as
though it were new. A deferral now leaves `last_error` alone: the row keeps
saying what it last actually failed with, which is worth more to an operator
than a note about a race that is already over.

An alert now quotes the row rather than the outcome where the outcome carries
only a shorthand. The name-conflict paths return the bare word `name_conflict`
and then park the row for good, so that one alert is the only signal the
operator will ever get, and it has to name the directory in the way. The row's
own text is only preferred when the repair actually wrote a status: when the
row is still `correcting`, nothing was written and the row is still carrying
the previous failure, which must not be reported as the current one.

A target that converges while its repair is in flight is no longer reported.
The drift the failure belonged to no longer exists, so paging the operator
about it is stale news.

A repair that throws is now logged with its stack as well as alerted. This
boundary cannot tell an operational failure from a bug in the repair and both
arrive identically; an alert alone would repeat a bug every tick and never
reach a developer, and re-throwing would strand the row again.

Separately, the drift observation restamped `last_drift_at` every time it
re-observed the same drift, so a drift outliving one tick reported itself as
just found, forever. It is stamped only when the row was not already drifted.
Both observation sites did this, the Inline one and the Git-managed one.

Coverage added for the Git-managed repair path, which is the third dispatch and
had no case, for the timestamp, and for the three alert rules above. 19 cases
now, 5 of which fail with the whole change reverted.
2026-09-28 19:54:31 -04:00
..