carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 21579854f6d59aab10dfe70b1f7a995b8da7700d parent 1676eb6
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)
+ })
+ }
+}