moves: pin the fail-closed clean-return and revert probes (Task 7b fix round 1, item 2)
platform-1e's ruling: a clean return's probe and a revert's probe never run configured, so no render hash is journalled under their hold op. With the recovery release missing from live/<host>, the probe's release adapter gets a typed not-yet and asks root nothing, root asked directly answers not-yet before any selection, and nothing is published. Known limitation: such a probe would need to journal its own render hash through RenderHeldUnderBefore at an equivalent point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+65
−0
internal/host/movesheldrelease_agent_test.go+65 −0
diff --git a/internal/host/movesheldrelease_agent_test.go b/internal/host/movesheldrelease_agent_test.go| index f3bdded4b..da744acb3 100644 |
| --- a/internal/host/movesheldrelease_agent_test.go |
| +++ b/internal/host/movesheldrelease_agent_test.go |
| @@ -319,3 +319,68 @@ func TestHeldReleaseAgentVerifiesRootsStage(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// TestHeldReleaseRecoveryProbesFailClosed (fix round 1, item 2; |
| +// platform-1e's ruling): a clean return's probe and a revert's probe never |
| +// run configured, so nothing journals a render hash under their hold op. |
| +// With the recovery release missing from live/<host>, each fails closed: the |
| +// probe's release adapter gets a typed not-yet and asks root nothing, root |
| +// asked directly answers not-yet before any selection, and nothing is |
| +// published. A known limitation: such a probe would need to journal its own |
| +// render hash through RenderHeldUnderBefore at an equivalent point. |
| +func TestHeldReleaseRecoveryProbesFailClosed(t *testing.T) { |
| + ctx := context.Background() |
| + recovery := func(f *hrFixture) { |
| + f.claim.Op.Exec, f.claim.Op.Participants = "uk-box", []string{"uk-box", "ie-box"} |
| + f.claim.Op.Intent = map[string]string{"source": "uk-box", "target": "ie-box", "to": "ie"} |
| + f.claim.Op.Phase = hostclaim.PhaseProbing |
| + f.rec.Move.From, f.rec.Move.To, f.rec.Move.LandingPhase = "uk", "ie", "" |
| + } |
| + for _, tc := range []struct { |
| + name string |
| + set func(f *hrFixture) |
| + hold string |
| + }{ |
| + {"a clean return's probe", func(f *hrFixture) { |
| + recovery(f) |
| + f.claim.Op.Kind = hostclaim.KindRestoring |
| + f.claim.Op.Intent["rollback"] = hrRB |
| + f.rec.Move.State = provision.MoveRollingBack |
| + f.rec.Move.Rollback = &provision.MoveRollback{ID: hrRB, SourcePhase: provision.RbSourceProbing, RecoveryInputs: &provision.MoveInputs{Release: "v7"}} |
| + f.row.HoldOp = hrRB |
| + }, hrRB}, |
| + {"a revert's probe", func(f *hrFixture) { |
| + recovery(f) |
| + f.claim.Op.Kind = hostclaim.KindReverting |
| + f.claim.Op.Journal = map[string]string{hostclaim.JournalRevPhase: hostclaim.PhaseProbing} |
| + f.rec.Move.State = provision.MoveReverting |
| + f.rec.Move.RecoveryInputs = &provision.MoveInputs{Release: "v7"} |
| + }, p1ID}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + f := newHRFixture(t) |
| + f.set(tc.set) |
| + // The recovery never ran configured: no render journal at all. |
| + f.data = t.TempDir() |
| + f.root.DataDir = f.data |
| + var seen []heldReleaseRequest |
| + adapter := heldReleaseProbeRelease(heldReleaseVia{Relay: f.relay(&seen), Region: "uk", Claims: f.claims}, f.s, "uk-box", f.opt, f.data, offProofGate(t)) |
| + rt := registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", HoldOp: tc.hold} |
| + if err := adapter(ctx, rt, "v7", heldSelectionWork{&heldSelections{}}); !errors.Is(err, errHeldReleaseNotYet) { |
| + t.Fatalf("the probe's release: %v, want errHeldReleaseNotYet", err) |
| + } |
| + if len(seen) != 0 { |
| + t.Fatalf("root was asked %+v", seen) |
| + } |
| + // Root asked directly, as a request carrying a render hash would. |
| + q := f.request(heldReleasePublish) |
| + if q.HoldOp != tc.hold { |
| + t.Fatalf("request hold op %q, want %q", q.HoldOp, tc.hold) |
| + } |
| + if _, err := f.root.Serve(ctx, hrPeer, q); !errors.Is(err, errHeldReleaseNotYet) { |
| + t.Fatalf("root: %v, want errHeldReleaseNotYet", err) |
| + } |
| + f.untouched(t) |
| + }) |
| + } |
| +} |