carlos / platform Public

park: local retirements under a move op keep the engine's layout; the journal names the op (§6.4, D6)

A park-local whose op is a move's id, under landing/committed and
restoring, lands under .parked/<incarnation>/<host>/<retirement>/artifacts/
and its .journal.json names the move's op.

A move leaves two local receipts, not one (K20): park.Local's journal and
the moves-owned rename's moves-retirement.json. The host test drives a
registered source's finalization, which writes both, and finds every one
of the move's local retirements by op with the same rule the park test
uses. Tests only; no interface changes (D6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5adc46fbb296e9e80c7c839d92a58442699fd3ef parent 69a93ad
2 files changed, +200 −2
  • internal/host/movesretire_byop_test.go +104 −0
  • internal/park/local_linux_test.go +96 −2
diff --git a/internal/host/movesretire_byop_test.go b/internal/host/movesretire_byop_test.go
new file mode 100644
index 000000000..56c03028c
--- /dev/null
+++ b/internal/host/movesretire_byop_test.go
@@ -0,0 +1,104 @@
+package host
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+)
+
+// retirementsByOp is how F3 finds a host's local retirements by op, one rule
+// for both receipts a retirement can carry (K20): park.Local's .journal.json
+// (Op) and the moves-owned rename's moves-retirement.json (op), each in the
+// event directory .parked/<incarnation>/<host>/<retirement>/. It returns the
+// retirement ids whose receipt names op, and those with no readable receipt.
+// internal/park's retirementsOf is the same rule.
+func retirementsByOp(t *testing.T, dataDir, incarnation, host, op string) (found, unreadable []string) {
+ t.Helper()
+ dir := filepath.Join(dataDir, ".parked", incarnation, host)
+ es, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, e := range es {
+ var engine struct{ Op string }
+ var owned struct {
+ Op string `json:"op"`
+ }
+ named := ""
+ if b, err := os.ReadFile(filepath.Join(dir, e.Name(), ".journal.json")); err == nil && json.Unmarshal(b, &engine) == nil {
+ named = engine.Op
+ } else if b, err := os.ReadFile(filepath.Join(dir, e.Name(), localParkReceiptName)); err == nil && json.Unmarshal(b, &owned) == nil {
+ named = owned.Op
+ } else {
+ unreadable = append(unreadable, e.Name())
+ continue
+ }
+ if named == op {
+ found = append(found, e.Name())
+ }
+ }
+ sort.Strings(found)
+ return found, unreadable
+}
+
+// TestMoveLocalRetirementsAreFindableByOp (moves spec §6.4, D6, K20): a
+// move leaves two local receipts, not one. A registered source's
+// finalization retires its installation through park.Local (.journal.json)
+// and parks what the inventory does not list by the moves-owned rename
+// (moves-retirement.json), each in its own retirement under the engine's
+// layout. Both name the move's op, and nothing under the host's
+// incarnation lacks a receipt, so F3 finds every local retirement of the
+// move by op with one rule.
+func TestMoveLocalRetirementsAreFindableByOp(t *testing.T) {
+ h := newFinalHarness(t)
+ dd := h.cfg.DataDir
+ for _, d := range []string{"hostguard", ".parked"} {
+ if err := os.MkdirAll(filepath.Join(dd, d), 0o770); err != nil {
+ t.Fatal(err)
+ }
+ }
+ rec := `{"v":1,"host":"` + p1Host + `","incarnation":"` + h.inc + `","active_stamp":1,"retired_stamps":[]}`
+ if err := os.WriteFile(filepath.Join(dd, "hostguard", p1Host+".json"), []byte(rec), 0o660); err != nil {
+ t.Fatal(err)
+ }
+ rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
+ if c.Inventory == nil {
+ c.Inventory = map[string]hostclaim.InventoryEntry{}
+ }
+ c.Inventory["ie-box"] = hostclaim.InventoryEntry{Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}}
+ })
+ h.commit(t)
+ var chore hostclaim.Obligation
+ for _, o := range h.claim(t).Obligations {
+ if o.Kind == "park-local" {
+ chore = o
+ }
+ }
+ if chore.Op != p1ID {
+ t.Fatalf("the commit queued no park-local of the move: %+v", h.claim(t).Obligations)
+ }
+ h.sourcePass(t)
+ owned := h.parkedJournal(t).Steps[finalStepLocal]
+ if owned == "" || owned == chore.Retirement {
+ t.Fatalf("the moves-owned retirement %q, park.Local's %q", owned, chore.Retirement)
+ }
+ for _, receipt := range []string{filepath.Join(chore.Retirement, ".journal.json"), filepath.Join(owned, localParkReceiptName)} {
+ if _, err := os.Stat(filepath.Join(dd, ".parked", h.inc, p1Host, receipt)); err != nil {
+ t.Fatalf("the receipt %s: %v", receipt, err)
+ }
+ }
+ found, unreadable := retirementsByOp(t, dd, h.inc, p1Host, p1ID)
+ want := []string{chore.Retirement, owned}
+ sort.Strings(want)
+ if strings.Join(found, " ") != strings.Join(want, " ") || len(unreadable) != 0 {
+ t.Fatalf("by op %s: found %v (want %v), no receipt %v", p1ID, found, want, unreadable)
+ }
+ if other, _ := retirementsByOp(t, dd, h.inc, p1Host, "mv_zzzzzzzzzzzzzzzzzzzzzzzzzz"); len(other) != 0 {
+ t.Fatalf("another op found the move's retirements: %v", other)
+ }
+}
diff --git a/internal/park/local_linux_test.go b/internal/park/local_linux_test.go
index aae79d7f9..106b2db64 100644
--- a/internal/park/local_linux_test.go
+++ b/internal/park/local_linux_test.go
@@ -27,6 +27,13 @@ type localFixture struct {
}
func localFixtureFor(t *testing.T, paths map[string]uint64) localFixture {
+ t.Helper()
+ return localFixtureForOp(t, paths, hostclaim.Op{Kind: hostclaim.KindParking, ID: "delete", Exec: "box"})
+}
+
+// localFixtureForOp is localFixtureFor under the claim op given; the
+// park-local chore is that op's.
+func localFixtureForOp(t *testing.T, paths map[string]uint64, op hostclaim.Op) localFixture {
t.Helper()
host := "shop.bab.oncarlos.com"
data, locks := t.TempDir(), t.TempDir()
@@ -43,9 +50,9 @@ func localFixtureFor(t *testing.T, paths map[string]uint64) localFixture {
}
c := &hostclaim.Claim{V: 2, Host: host, Gen: 5,
Holder: &hostclaim.Holder{Family: "instance", Key: "apps/bab/jam/instances/" + host + ".json", Account: "bab", App: "jam", CreatedAt: at, Incarnation: inc, Location: "instances/" + host + "/" + inc, Stamp: 5},
- Op: hostclaim.Op{Kind: hostclaim.KindParking, ID: "delete", Exec: "box"},
+ Op: op,
Inventory: map[string]hostclaim.InventoryEntry{"box": {Layout: "flat", Paths: paths}},
- Obligations: []hostclaim.Obligation{{ID: "local", Op: "delete", Kind: "park-local", Box: "box", Owner: "box:box", Retirement: "retirement"}}}
+ Obligations: []hostclaim.Obligation{{ID: "local", Op: op.ID, Kind: "park-local", Box: "box", Owner: "box:box", Retirement: "retirement"}}}
if err := c.Validate(); err != nil {
t.Fatal(err)
}
@@ -671,3 +678,90 @@ func TestLocalUntouchedBoxAcknowledgement(t *testing.T) {
})
}
}
+
+// retirementsOf is how F3 finds a host's local retirements by op, one rule
+// for both receipts a retirement can carry (K20): park.Local's .journal.json
+// (Op) and the moves-owned rename's moves-retirement.json (op), each in the
+// event directory .parked/<incarnation>/<host>/<retirement>/. It returns
+// the retirement ids whose receipt names op.
+func retirementsOf(t *testing.T, dataDir, incarnation, host, op string) []string {
+ t.Helper()
+ dir := filepath.Join(dataDir, ".parked", incarnation, host)
+ es, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var out []string
+ for _, e := range es {
+ var engine struct{ Op string }
+ var moves struct {
+ Op string `json:"op"`
+ }
+ named := ""
+ if b, err := os.ReadFile(filepath.Join(dir, e.Name(), ".journal.json")); err == nil && json.Unmarshal(b, &engine) == nil {
+ named = engine.Op
+ } else if b, err := os.ReadFile(filepath.Join(dir, e.Name(), "moves-retirement.json")); err == nil && json.Unmarshal(b, &moves) == nil {
+ named = moves.Op
+ }
+ if named == op {
+ out = append(out, e.Name())
+ }
+ }
+ return out
+}
+
+// TestLocalRetirementUnderAMoveOpIsFindableByOp (moves spec §6.4, D6, K20):
+// a park-local whose op is a move's id, under each move kind that queues
+// one, lands under the engine's own layout,
+// .parked/<incarnation>/<host>/<retirement>/artifacts/, and its journal names
+// the move's op, so F3 selects a move's local retirements by op as it does
+// the remote ones by manifest. The moves-owned rename's receipt is the
+// other half; internal/host's TestMoveLocalRetirementsAreFindableByOp
+// covers it with the same rule.
+func TestLocalRetirementUnderAMoveOpIsFindableByOp(t *testing.T) {
+ const move = "mv_abcdefghijklmnopqrstuvwxyz"
+ for _, op := range []hostclaim.Op{
+ {Kind: hostclaim.KindLanding, ID: move, Phase: hostclaim.PhaseCommitted, Exec: "box"},
+ {Kind: hostclaim.KindRestoring, ID: move, Phase: hostclaim.PhaseClaimed, Exec: "box"},
+ } {
+ for _, mode := range []string{"artifacts", "installation"} {
+ t.Run(op.Kind+"/"+mode, func(t *testing.T) {
+ f := localFixtureForOp(t, map[string]uint64{"old.db": 3, "nested/state": 3}, op)
+ for _, p := range []string{"old.db", "nested/state"} {
+ f.write(t, p, p)
+ }
+ var err error
+ if mode == "artifacts" {
+ err = f.l.RetireArtifacts(t.Context(), f.host, f.captured, "local", move, 3)
+ } else {
+ err = f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", move)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, p := range []string{"old.db", "nested/state"} {
+ if b, err := os.ReadFile(f.parked(p)); err != nil || string(b) != p {
+ t.Fatalf("not under the engine's layout: %s: %q %v", p, b, err)
+ }
+ }
+ b, err := os.ReadFile(filepath.Join(f.l.DataDir, ".parked", f.captured.Incarnation, f.host, "retirement", ".journal.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var j localJournal
+ if err := json.Unmarshal(b, &j); err != nil {
+ t.Fatal(err)
+ }
+ if j.Op != move || j.Host != f.host || j.Incarnation != f.captured.Incarnation || j.Retirement != "retirement" || j.Obligation != "local" || j.Mode != mode {
+ t.Fatalf("the journal does not name the move: %+v", j)
+ }
+ if got := retirementsOf(t, f.l.DataDir, f.captured.Incarnation, f.host, move); len(got) != 1 || got[0] != "retirement" {
+ t.Fatalf("retirements of %s: %v", move, got)
+ }
+ if got := retirementsOf(t, f.l.DataDir, f.captured.Incarnation, f.host, "delete"); len(got) != 0 {
+ t.Fatalf("another op found the move's retirement: %v", got)
+ }
+ })
+ }
+ }
+}