carlos / platform Public

moves: a dirty return whose target never admitted parks nothing (Task 9a fix round 3)

Ruling (a). A dirty return's parking parks the pre-move generations at the
source's location. When the target never admitted this move, the holder's
generation is still the pre-move one (ReturnToSource leaves it for a dirty
return), the source's live state, and its residue park was refused for
ever.

- installRun.parking, for a return (roleSource) whose residue list is not
  yet journalled, decides from recorded evidence only (targetAdmitted):
  the record's landing reached probing, or the claim's holder has an
  admission of the move's prepared generation. Without either, it
  journals residue "[]" and residue_kept (the sentence why), logs it, and
  advances: no chore, the pre-move generation kept.
- A park view's refusal never decides it: with admission evidence the
  return parks exactly as before, and a refusal makes it wait.
- No claim code changed.

Tests, through the real view and engine:
TestDirtyReturnWithoutAdmissionKeepsThePreMoveGeneration (the return
completes its install, the pre-move bytes are intact, the journal line is
present, no residue chore), TestReturnResidueParkRunsThroughTheInstall
(with the target's admission: parked, archived and retired, no no-op line),
TestReturnResidueParkWaitsOnATransientRefusal (admission evidence and a
refusing view: waits at parking, no no-op line, bytes intact; parks once
the view answers). The return harness records the target's admission by
default (newReturnHarnessWith). RED: the no-evidence return waited on
"park generation cur: park: unsafe or incomplete input".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 854597a71c2c8f90f739c13a6f4a96e9a5a1d01f parent b2700e6
3 files changed, +169 −35
  • internal/host/movesfileslive_test.go +102 −35
  • internal/host/movesinstall.go +45 −0
  • internal/host/movesinstall_test.go +22 −0
diff --git a/internal/host/movesfileslive_test.go b/internal/host/movesfileslive_test.go
index 14ebdf121..97d4cc0a1 100644
--- a/internal/host/movesfileslive_test.go
+++ b/internal/host/movesfileslive_test.go
@@ -451,60 +451,127 @@ func TestRollbackReturnCleanupKeepsTheFilesLiveSet(t *testing.T) {
}
}
+// returnPreMove lists the pre-move generation's objects at the source's
+// location, with their bodies.
+func returnPreMove(t *testing.T, h *returnHarness, location string) map[string]string {
+ t.Helper()
+ out := map[string]string{}
+ for k, o := range h.f.buckets["data-ie"] {
+ if strings.HasPrefix(k, location+"/g-cur/") {
+ out[k] = o.body
+ }
+ }
+ if len(out) == 0 {
+ t.Fatal("fixture: no pre-move generation at the source's location")
+ }
+ return out
+}
+
// TestReturnResidueParkRunsThroughTheInstall (fix round 2, I3; the source's
-// return-residue park, movesinstall.go parking): a dirty return's parking
-// phase runs its residue chores through the installer's own engine with the
-// real view. The boundary is the target's admission: before it, the
-// pre-move generation is still holder.generation, authoritative in the
-// source's region, so its residue park is refused and its bytes stay; once
-// the target's probe was admitted (the holder's generation is the target's,
-// authoritative in uk) the same parking archives the pre-move generation
-// and retires it from the source's location.
+// return-residue park, movesinstall.go parking): with the record's evidence
+// that the target admitted this move (its probe's admission of the move's
+// prepared generation), a dirty return's parking runs its residue chores
+// through the installer's own engine with the real view: the pre-move
+// generation is archived under parked/ and retired from the source's
+// location, and the return moves on.
func TestReturnResidueParkRunsThroughTheInstall(t *testing.T) {
h := newReturnHarness(t, false)
location := strings.TrimSuffix(h.claim(t).Holder.Location, "/")
- var pre []string
- for k := range h.f.buckets["data-ie"] {
- if strings.HasPrefix(k, location+"/g-cur/") {
- pre = append(pre, k)
+ pre := returnPreMove(t, h, location)
+ h.rcfg.ParkView = movesParkView(MovesRecordsFrom(h.s), h.vs, "data-ie", "ie")
+ if _, err := h.install(t); err != nil {
+ t.Fatalf("the return with the target's admission: %v", err)
+ }
+ if rb := h.read(t, p1Host).Move.Rollback; rb.SourcePhase == provision.RbSourceParking {
+ t.Fatalf("the return is still parking: %+v", rb)
+ }
+ for k := range pre {
+ if _, ok := h.f.buckets["data-ie"][k]; ok {
+ t.Fatalf("%s was not retired", k)
+ }
+ if !archivedUnder(h.f, "data-ie", h.inc, location, k) {
+ t.Fatalf("%s was not archived", k)
}
}
- if len(pre) == 0 {
- t.Fatal("fixture: no pre-move generation at the source's location")
+ if j := h.returnJournal(t); j.Steps[installStepResidueKept] != "" {
+ t.Fatalf("a return with admission evidence journalled the no-op: %q", j.Steps[installStepResidueKept])
}
+}
+
+// TestDirtyReturnWithoutAdmissionKeepsThePreMoveGeneration (fix round 3,
+// ruling (a)): the record carries no evidence that the target admitted this
+// move (the landing never reached probing, and the claim holds no admission
+// of the move's generation). The pre-move generation is still the source's
+// live state, so the return's parking is a journalled no-op: through the
+// real view and engine the return completes, the pre-move bytes are intact
+// and no residue park is queued.
+func TestDirtyReturnWithoutAdmissionKeepsThePreMoveGeneration(t *testing.T) {
+ h := newReturnHarnessWith(t, false, false)
+ location := strings.TrimSuffix(h.claim(t).Holder.Location, "/")
+ pre := returnPreMove(t, h, location)
h.rcfg.ParkView = movesParkView(MovesRecordsFrom(h.s), h.vs, "data-ie", "ie")
- before := map[string]string{}
- for _, k := range pre {
- before[k] = h.f.buckets["data-ie"][k].body
+ if _, err := h.install(t); err != nil {
+ t.Fatalf("the return without admission evidence: %v", err)
}
+ if rb := h.read(t, p1Host).Move.Rollback; rb.SourcePhase != provision.RbSourceRestored {
+ t.Fatalf("the return did not complete its install: %+v", rb)
+ }
+ for k, body := range pre {
+ if got, ok := h.f.buckets["data-ie"][k]; !ok || got.body != body {
+ t.Fatalf("%s changed", k)
+ }
+ }
+ j := h.returnJournal(t)
+ if got := j.Steps[installStepResidueKept]; !strings.Contains(got, "never admitted") {
+ t.Fatalf("the journal's no-op line: %q", got)
+ }
+ if got := j.Steps[landStepResidue]; got != "[]" {
+ t.Fatalf("the residue list %q, want none", got)
+ }
+ for _, o := range h.claim(t).Obligations {
+ if o.Kind == "park-generation" {
+ t.Fatalf("a residue park was queued: %+v", o)
+ }
+ }
+}
- // Before the target's admission: refused, bytes intact, still parking.
- if _, err := h.install(t); !errors.Is(err, park.ErrUnsafe) {
- t.Fatalf("the return's residue park before the target's admission: %v", err)
+// TestReturnResidueParkWaitsOnATransientRefusal (fix round 3): with the
+// target's admission recorded, a view refusal never turns the parking into
+// the no-op. The return waits at parking with the pre-move bytes intact and
+// no no-op line, and parks once the view answers.
+func TestReturnResidueParkWaitsOnATransientRefusal(t *testing.T) {
+ h := newReturnHarness(t, false)
+ location := strings.TrimSuffix(h.claim(t).Holder.Location, "/")
+ pre := returnPreMove(t, h, location)
+ real := movesParkView(MovesRecordsFrom(h.s), h.vs, "data-ie", "ie")
+ down := true
+ h.rcfg.ParkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
+ if down {
+ return park.View{}, fmt.Errorf("%w: the record read timed out", park.ErrUnsafe)
+ }
+ return real(ctx, obs)
+ }
+ if _, err := h.install(t); err == nil {
+ t.Fatal("the return went on through a view refusal")
}
if rb := h.read(t, p1Host).Move.Rollback; rb.SourcePhase != provision.RbSourceParking {
- t.Fatalf("the return went past parking: %+v", rb)
+ t.Fatalf("the return left parking: %+v", rb)
+ }
+ if j := h.returnJournal(t); j.Steps[installStepResidueKept] != "" {
+ t.Fatalf("a view refusal became the no-op: %q", j.Steps[installStepResidueKept])
}
- for k, body := range before {
+ for k, body := range pre {
if got, ok := h.f.buckets["data-ie"][k]; !ok || got.body != body {
- t.Fatalf("%s changed before the boundary", k)
+ t.Fatalf("%s changed during the refusal", k)
}
}
-
- // After it: the target's probe was admitted in uk.
- admitTarget(t, h.landingHarness)
+ down = false
if _, err := h.install(t); err != nil {
- t.Fatalf("the return after the target's admission: %v", err)
- }
- if rb := h.read(t, p1Host).Move.Rollback; rb.SourcePhase == provision.RbSourceParking {
- t.Fatalf("the return is still parking: %+v", rb)
+ t.Fatalf("after the refusal: %v", err)
}
- for _, k := range pre {
+ for k := range pre {
if _, ok := h.f.buckets["data-ie"][k]; ok {
- t.Fatalf("%s was not retired", k)
- }
- if !archivedUnder(h.f, "data-ie", h.inc, location, k) {
- t.Fatalf("%s was not archived", k)
+ t.Fatalf("%s was not retired once the view answered", k)
}
}
}
diff --git a/internal/host/movesinstall.go b/internal/host/movesinstall.go
index 124341716..6ff345b81 100644
--- a/internal/host/movesinstall.go
+++ b/internal/host/movesinstall.go
@@ -69,6 +69,11 @@ import (
// artifacts (parkLocalArtifacts' step: the retirement id).
const installStepPreMove = "pre_move_parked"
+// installStepResidueKept is a dirty return's no-op parking (Task 9a fix
+// round 3, ruling (a)): the sentence saying the target never admitted the
+// move, so the pre-move generation stays the source's live state.
+const installStepResidueKept = "residue_kept"
+
// installStamp is what a phase step records besides the phase: at the CAS
// that ends configured, the installed database's digest (a landing records
// it in landing.installed_db_sha256) and the inputs a return rendered.
@@ -394,6 +399,23 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) {
if err != nil {
return false, err
}
+ _, listed := j.Steps[landStepResidue]
+ if !listed && ir.src.Role == roleSource && !targetAdmitted(ir.rec.Move, obs.Claim) {
+ // A dirty return whose target never admitted this move (ruling
+ // (a), fix round 3): holder.generation is still the pre-move one,
+ // the source's live state, so there is no residue to park. This
+ // is decided from recorded evidence only, never from a park view's
+ // refusal, which can be transient.
+ why := fmt.Sprintf("the target never admitted move %s: no probe ran and the claim holds no admission of its generation; the pre-move generation stays the source's live state", id)
+ j.Phase = phase
+ j.Mark(landStepResidue, "[]")
+ j.Mark(installStepResidueKept, why)
+ if err := j.Write(cfg.DataDir); err != nil {
+ return false, err
+ }
+ log.Printf("moves: %s: the %s parks nothing: %s", host, ir.src.Name, why)
+ return ir.advance(ctx, phase, next, installStamp{})
+ }
var gens []string
if raw, ok := j.Steps[landStepResidue]; ok {
if err := json.Unmarshal([]byte(raw), &gens); err != nil {
@@ -490,6 +512,29 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) {
return ir.advance(ctx, phase, next, installStamp{})
}
+// targetAdmitted reports whether the record or the claim carries evidence
+// that the move's target admitted a writer (fix round 3): the record's
+// landing reached probing, whose probe is the target's only admission, or
+// the holder's admissions include the move's prepared generation. Without
+// either, nothing of the target's ever became holder.generation.
+func targetAdmitted(m *provision.MoveState, c *hostclaim.Claim) bool {
+ if m == nil {
+ return false
+ }
+ order := provision.LandingOrder()
+ if landingIndex(order, m.LandingPhase) >= landingIndex(order, provision.LandProbing) {
+ return true
+ }
+ if c != nil && c.Holder != nil {
+ for _, a := range c.Holder.Admissions {
+ if a.Generation == m.ID {
+ return true
+ }
+ }
+ }
+ return false
+}
+
// building (§7.6, §5.1 step 2): the generation built from the seal's pinned
// replica versions under the location's lease (moves.BuildGeneration: the
// intent, the snapshot, the anchor, the completed install manifest, every
diff --git a/internal/host/movesinstall_test.go b/internal/host/movesinstall_test.go
index 126fe1a8b..2143b5c2b 100644
--- a/internal/host/movesinstall_test.go
+++ b/internal/host/movesinstall_test.go
@@ -56,10 +56,32 @@ type returnHarness struct {
// flat paths in the claim's inventory at S and queues its park-local, as
// Task 5's claimed does when the inventory lists them (J13).
func newReturnHarness(t *testing.T, registered bool) *returnHarness {
+ t.Helper()
+ return newReturnHarnessWith(t, registered, true)
+}
+
+// returnJournal is the return's own op journal on the source.
+func (h *returnHarness) returnJournal(t *testing.T) *moves.Journal {
+ t.Helper()
+ j, err := moves.ReadJournal(h.sdir, p1Host, p1RB)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return j
+}
+
+// newReturnHarnessWith is newReturnHarness with the target's admission
+// recorded or not (fix round 3): admitted, the claim holds the target's
+// probe admission of the move's prepared generation, the evidence a dirty
+// return needs before it parks the pre-move generations.
+func newReturnHarnessWith(t *testing.T, registered, admitted bool) *returnHarness {
t.Helper()
ctx := context.Background()
h := &returnHarness{landingHarness: newLandingHarness(t)}
h.flip(t)
+ if admitted {
+ admitTarget(t, h.landingHarness)
+ }
h.sdir = h.cfg.DataDir
rec := h.read(t, p1Host)
h.baseline = *rec.Move.Source