| index 19d375a39..20768fd83 100644 |
| --- a/internal/hostclaim/move_test.go |
| +++ b/internal/hostclaim/move_test.go |
| @@ -1155,3 +1155,150 @@ func TestParkQueueHelpersPreserveTheRestOfTheClaim(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +// probedLanding drives the claim to landing/probed the way the landing does: |
| +// the flip, then every record phase mirrored through SetMovePhase. The |
| +// target's probe admission is on the holder, its predecessor in region ie. |
| +func probedLanding(t *testing.T) *Claim { |
| + t.Helper() |
| + c := movingToSealed(t) |
| + c.Gen = 7 |
| + if err := Flip(moveA, "uk-box", 3, "gen-7")(c); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, step := range [][2]string{{"checking", "parking"}, {"parking", "building"}, {"building", "published"}, {"published", "local"}, {"local", "configured"}, {"configured", "restored"}, {"restored", "probing"}, {"probing", "probed"}} { |
| + if err := SetMovePhase(moveA, KindLanding, step[0], step[1])(c); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + c.Holder.Session = 9 |
| + c.Holder.Admissions = []AdmissionRecord{{Region: "uk", PredecessorRegion: "ie", Session: 9, Box: "uk-box", Generation: moveA, Predecessor: "gen-7", Mode: "cold", Coverage: "sealed"}} |
| + return c |
| +} |
| + |
| +func commitScopes(c *Claim) (src, stg ParkScope) { |
| + at := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC) |
| + src = ParkScope{Location: c.Holder.Location, Incarnation: c.Holder.Incarnation, EvidenceAt: at, RequestLimit: 120 * time.Second} |
| + stg = ParkScope{Location: "moves/" + c.Host + "/" + moveA, Incarnation: c.Holder.Incarnation, EvidenceAt: at, RequestLimit: 120 * time.Second} |
| + return src, stg |
| +} |
| + |
| +func obligationKinds(c *Claim) map[string]int { |
| + kinds := map[string]int{} |
| + for _, o := range c.Obligations { |
| + kinds[o.Kind+"/"+o.Region+o.Box]++ |
| + } |
| + return kinds |
| +} |
| + |
| +// wCommitCoverage, wCommitReplayed, H3, H5(b). |
| +func TestCommitQueuesObligationsAndCoversTheSourceRegion(t *testing.T) { |
| + for _, from := range []string{"probing", "probed"} { |
| + c := probedLanding(t) |
| + c.Op.Phase = from |
| + // The source box holds paths of the stay the move leaves: stamp S = 3. |
| + c.Inventory = map[string]InventoryEntry{"ie-box": {Layout: "flat", Paths: map[string]uint64{"/data/a": 3}}} |
| + stamp, session, gen := c.Holder.Stamp, c.Holder.Session, c.Holder.Generation |
| + ids := 0 |
| + newID := func() string { ids++; return fmt.Sprintf("id%d", ids) } |
| + src, stg := commitScopes(c) |
| + if err := Commit(moveA, "ie", "ie-box", "uk", src, stg, newID)(c); err != nil { |
| + t.Fatalf("from %s: %v", from, err) |
| + } |
| + if c.Op.Kind != KindLanding || c.Op.Phase != PhaseCommitted || c.Op.Exec != "uk-box" { |
| + t.Fatalf("after commit: %+v", c.Op) |
| + } |
| + // H3: the holder's stamp, session and generation are the flip's, untouched. |
| + if c.Holder.Stamp != stamp || c.Holder.Session != session || c.Holder.Generation != gen { |
| + t.Fatalf("commit touched the holder: %+v", c.Holder) |
| + } |
| + k := obligationKinds(c) |
| + if k["park-replica/ie"] != 1 || k["park-local/ie-box"] != 1 || k["park-replica/uk"] != 1 || len(c.Obligations) != 3 { |
| + t.Fatalf("obligations: %+v", c.Obligations) |
| + } |
| + for _, o := range c.Obligations { |
| + if o.Kind == "park-replica" && o.Region == "ie" && (o.Park == nil || o.Park.Location != c.Holder.Location || o.Owner != "region:ie") { |
| + t.Fatalf("source park: %+v", o) |
| + } |
| + if o.Kind == "park-replica" && o.Region == "uk" && (o.Park == nil || o.Park.Location != stg.Location) { |
| + t.Fatalf("staging park: %+v", o) |
| + } |
| + } |
| + if !c.Holder.Admissions[0].Covered { |
| + t.Fatal("the source-region predecessor is not covered (wCommitCoverage)") |
| + } |
| + if err := c.Validate(); err != nil { |
| + t.Fatalf("the committed claim must validate: %v", err) |
| + } |
| + // Replay: nothing doubles and no ids are minted (wCommitReplayed). |
| + before, usedIDs := mustJSON(t, c), ids |
| + if err := Commit(moveA, "ie", "ie-box", "uk", src, stg, newID)(c); err != nil || ids != usedIDs || string(mustJSON(t, c)) != string(before) { |
| + t.Fatalf("replay: %v ids %d->%d", err, usedIDs, ids) |
| + } |
| + } |
| +} |
| + |
| +// H5(b): the source's park-local is queued only when the source box lists |
| +// paths of the journalled source stamp S. |
| +func TestCommitQueuesTheSourceParkLocalOnlyForTheSourceStamp(t *testing.T) { |
| + for name, inv := range map[string]map[string]InventoryEntry{ |
| + "no inventory": nil, |
| + "other stamp only": {"ie-box": {Layout: "flat", Paths: map[string]uint64{"/data/a": 2}}}, |
| + "stamp on another box": {"uk-box": {Layout: "flat", Paths: map[string]uint64{"/data/a": 3}}}, |
| + } { |
| + c := probedLanding(t) |
| + c.Inventory = inv |
| + ids := 0 |
| + newID := func() string { ids++; return fmt.Sprintf("id%d", ids) } |
| + src, stg := commitScopes(c) |
| + if err := Commit(moveA, "ie", "ie-box", "uk", src, stg, newID)(c); err != nil { |
| + t.Fatalf("%s: %v", name, err) |
| + } |
| + k := obligationKinds(c) |
| + if k["park-replica/ie"] != 1 || k["park-replica/uk"] != 1 || len(c.Obligations) != 2 { |
| + t.Fatalf("%s: obligations %+v", name, c.Obligations) |
| + } |
| + if !c.Holder.Admissions[0].Covered { |
| + t.Fatalf("%s: the source region is not covered", name) |
| + } |
| + } |
| +} |
| + |
| +// wCommitInterrupted: a commit that is refused leaves the claim as it was. |
| +func TestCommitRefusesAnythingButAProbingTargetLanding(t *testing.T) { |
| + newID := func() string { return "x" } |
| + type commitFn = func(c *Claim, src, stg ParkScope) func(*Claim) error |
| + std := func(op, region, box string) commitFn { |
| + return func(c *Claim, src, stg ParkScope) func(*Claim) error { |
| + return Commit(op, region, box, "uk", src, stg, newID) |
| + } |
| + } |
| + for name, tc := range map[string]struct { |
| + mutate func(c *Claim, src, stg *ParkScope) |
| + fn commitFn |
| + want error |
| + }{ |
| + "serving claim": {func(c *Claim, _, _ *ParkScope) { c.Op = Op{Kind: KindServing, Exec: "ie-box"} }, std(moveA, "ie", "ie-box"), ErrStaleOp}, |
| + "another op": {func(*Claim, *ParkScope, *ParkScope) {}, std(moveB, "ie", "ie-box"), ErrStaleOp}, |
| + "moving": {func(c *Claim, _, _ *ParkScope) { c.Op.Kind, c.Op.Phase, c.Op.Exec = KindMoving, PhaseSealed, "" }, std(moveA, "ie", "ie-box"), ErrWrongState}, |
| + "landing before probing": {func(c *Claim, _, _ *ParkScope) { c.Op.Phase = "restored" }, std(moveA, "ie", "ie-box"), ErrWrongState}, |
| + "exec not the target": {func(c *Claim, _, _ *ParkScope) { c.Op.Exec = "ie-box" }, std(moveA, "ie", "ie-box"), ErrWrongState}, |
| + "exec none": {func(c *Claim, _, _ *ParkScope) { c.Op.Exec = "" }, std(moveA, "ie", "ie-box"), ErrWrongState}, |
| + "source box is not the source": {func(*Claim, *ParkScope, *ParkScope) {}, std(moveA, "ie", "uk-box"), ErrWrongState}, |
| + "source region is the target": {func(*Claim, *ParkScope, *ParkScope) {}, std(moveA, "uk", "ie-box"), ErrWrongState}, |
| + "source park is not the holder's location": {func(_ *Claim, s, _ *ParkScope) { s.Location = "instances/a.go.example.com/other" }, std(moveA, "ie", "ie-box"), ErrMalformed}, |
| + "staging park is another lane": {func(c *Claim, _, g *ParkScope) { g.Location = "moves/" + c.Host + "/" + moveB }, std(moveA, "ie", "ie-box"), ErrMalformed}, |
| + } { |
| + c := probedLanding(t) |
| + c.Inventory = map[string]InventoryEntry{"ie-box": {Layout: "flat", Paths: map[string]uint64{"/data/a": 3}}} |
| + src, stg := commitScopes(c) |
| + tc.mutate(c, &src, &stg) |
| + before := mustJSON(t, c) |
| + if err := tc.fn(c, src, stg)(c); !errors.Is(err, tc.want) { |
| + t.Fatalf("%s: got %v, want %v", name, err, tc.want) |
| + } |
| + if after := mustJSON(t, c); string(after) != string(before) { |
| + t.Fatalf("%s mutated the claim:\nbefore %s\nafter %s", name, before, after) |
| + } |
| + } |
| +} |