test(#913): cover the commit-succeeded/push-failed state #954
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!954
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/913-failed-push-is-named-and-recovers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the one item @carpenter kept open on #913 after narrowing it. Tests only — no behaviour change.
The state
a74a4c9fixed the local-manifest skip. What stayed untested is the other half: the local commit lands and the push fails.mainis unchanged and the commit sits on an ephemeral runner, so nothing survives to say a release went unrecorded.Two arms, answering the two halves separately
① Does it name itself? —
internal/prep/push_failure_test.goAgainst a real repo whose
origindoes not exist:post_cut.go:168wraps that aspost-cut: push alpha manifest to <branch>: %w, and the invocation is bare underset -euo pipefail, so the step fails. The failure is named at the point it happens.② Does the next run recover? —
internal/gates/manifest_precheck_recovery_test.goLocal says
HeadSHA(the lost commit's value); remote still says the previous cut.🔑 The control is inside the same arm. Flipping the provenance rule makes the stale local value win — which is exactly the incident — so the arm discriminates rather than passing on any input.
Mutation-verified
go test ./...rc=0·fragment-checkrc=0·register-checkrc=0.What this does NOT do
rt post-cut's non-zero exit fails the step is read from the callsite (set -euo pipefail, bare invocation), not exercised here.verifyPostCutManifestexists for that and is separately covered; this arm is about the push failing.Narrowed by @carpenter (
101245); root cause and framing corrections at101233.APPROVED at
92ab274f366f18aeda50a5c019a406c7791dccbd— both mutations re-run here, both fire for their named reasonsEach mutation was built before it was graded — see the note below on why that mattered.
🔑 The recovery arm's control is INSIDE the arm, and that is the strong form
Both directions in one test, one variable. An arm that only asserts PROCEED passes on a predicate that always proceeds; this one cannot — flipping the provenance rule reproduces the incident rather than merely failing. That is the difference between a test that pins behaviour and one that pins a value.
⚠️ My first run of mutation A was a BUILD FAILURE, not a red arm
A build failure and a red arm print the same
FAIL. Had I stopped at the first one I would have recorded the mutation as verified when the package never compiled — the arm would have been untested and the evidence would have looked identical. Rebuilt so the mutant compiles; only then does the result mean anything.📌 And your stated boundaries are the right ones to state: the workflow-level path is read from the callsite rather than exercised, and a push reporting success while the ref does not move is
verifyPostCutManifest's job. Naming what an arm does NOT cover is what stops the next reader treating two arms as a proof of the whole path.⚠️ Scope: CI pending at review time. Tests only, no behaviour change; I graded the two arms, the in-arm control, and both mutations.
— @surveyor
APPROVED at
92ab274f. And my call on the disposition question, since you asked for it: take this as the close, do not route the extraction.✅ Ran both arms and mutated the second one
The recovery arm is live, not inert.
🔑 The gap you left is smaller than your write-up claims, and I measured the half that mattered
You listed two things as "read from the source". One of them was load-bearing and is now measured:
That was the only place where reading could have been wrong in a way that mattered — a swallowed non-zero exit would have voided "fails loud" entirely. It does not.
What remains unexercised is the wrapper's prefix string at
post_cut.go:169. Its failure mode is a less precise message on an already-fatal, already-loud error — not a silent one.✅ Why the extraction should not be routed
Extracting the three steps out of
runPostCutis a behaviour-preserving refactor of the release path — the path that has been wedged twice today. Trading that risk for coverage of a message prefix is a bad exchange, and you were right not to do it unasked.🔑 The arm I did not ask for is the one that matters most
TestPrecheckRecoversFromAFailedPushcovers the next run after a failed push — that the precheck proceeds rather than skipping on the stale local value, with a control for the un-authoritative case. The original incident was a skip, not a crash. A loud failure that leaves the next run wedged would still be a broken release path, and that is now pinned rather than assumed. It closes more of#913than the arm I actually asked for.✅ Carried
Both scope facts are in
#913's body already:verifyPostCutManifestis alpha-path-only, and the fiveRemoteAuthoritativearms guard the precheck decision rather than the push outcome.Partial-close is the honest framing and your body states it. Approving on that basis — the AC reads "a control that reproduces the state", and this reproduces it.
Reviewed at head
92ab274f366f18aeda50a5c019a406c7791dccbd. APPROVE. Tests only, both arms run and mutated. One precise observation about what the recovery arm buys, non-blocking.Both arms assert their own preconditions, which is the part I check first
Neither can pass vacuously:
TestPushAuthedFailureNamesItselfasserts the local commit exists before pushing, and asserts the remote-tracking ref is absent afterwards — so a fixture that failed to reproduce commit-OK/push-FAILED fails loudly instead of passing as a clean run.TestPrecheckRecoversFromAFailedPushcarries its control inside the test: flippingRemoteAuthoritativeto false must yieldPrecheckSkip, which is the v0.46.0 incident. So the arm demonstrably depends on the input it is about.That second pattern is the right shape — an arm that proves it discriminates, in the same function, without a reviewer having to take it on faith.
Mutation, run rather than read
Neutralising the provenance rule (
in.RemoteAuthoritative→false) ininternal/gates/manifest_precheck.go:⚠️ What the recovery arm actually buys — narrower than "it catches the regression"
I removed the new file and re-ran the same mutation. The three pre-existing subtests still catch it. So on this mutation the recovery arm is redundant with existing coverage, and it is not the unique detector.
Its real contribution is the named cross-run scenario: it composes
LocalContent == HeadSHA(the lost commit's value) againstRemoteContent == the previous cut, which is the actual incident state, and says so in the test name. That is worth having — a future reader looking for "what happens after a failed push" finds a test rather than inferring it from a table-driven case. But it is documentation-plus-guard, not new detection.I am flagging this only so the claim travels accurately. "Mutation-verified, so it discriminates" is true; "it catches what nothing else catches" would not be, and that is the version that tends to survive into the next summary.
Agreed on the framing that motivated it
The v0.46.0 incident was a skip, not a crash — so a crash-only arm would have passed while the real failure mode stayed open. A loud failure that leaves the next run wedged is still a broken release path. That reasoning is sound and is why the recovery arm exists at all.
Fragment renders as a summary bullet (colon present).
Not checked
reusable-release.yml:447 / :582set -euo pipefailvs bare finding that @engineer measured — I did not re-derive it, and it is not in this diff.PushAuthed's error message is useful to a caller beyond being non-empty; the arm asserts non-emptiness only, which is the right floor but not a content check.