//go:build linux package park import ( "context" "encoding/json" "errors" "os" "os/exec" "path/filepath" "syscall" "testing" "time" "golang.org/x/sys/unix" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" ) type localFixture struct { l Local host string captured hostguard.Captured } 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() for _, name := range []string{"hostguard", ".parked"} { if err := os.Mkdir(filepath.Join(data, name), 0770); err != nil { t.Fatal(err) } } s := release.NewDirStore(t.TempDir()) at := time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC) inc, err := hostclaim.Incarnation("bab", "jam", host, at) if err != nil { t.Fatal(err) } 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: op, Inventory: map[string]hostclaim.InventoryEntry{"box": {Layout: "flat", Paths: paths}}, 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) } b, err := json.Marshal(c) if err != nil { t.Fatal(err) } key, err := hostclaim.Key(host) if err != nil { t.Fatal(err) } if _, err := s.Put(t.Context(), key, b, ""); err != nil { t.Fatal(err) } g := hostguard.Guard{DataDir: data, LockDir: locks, Claims: hostguard.StoreReader{Store: s}} if err := g.Activate(t.Context(), host, "box", nil); err != nil { t.Fatal(err) } capture, err := hostguard.Capture(data, host) if err != nil { t.Fatal(err) } return localFixture{l: Local{Authorize: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, Guard: g, Claims: s, DataDir: data, Box: "box"}, host: host, captured: capture} } func (f localFixture) write(t *testing.T, name, body string) { t.Helper() p := filepath.Join(f.l.DataDir, name) if err := os.MkdirAll(filepath.Dir(p), 0770); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(body), 0660); err != nil { t.Fatal(err) } } func (f localFixture) parked(name string) string { return filepath.Join(f.l.DataDir, ".parked", f.captured.Incarnation, f.host, "retirement", "artifacts", name) } func TestLocalArtifactStampAndInstallationRetirement(t *testing.T) { for _, mode := range []string{"artifacts", "installation"} { t.Run(mode, func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"old.db": 3, "new.db": 5, "nested/state": 3}) for _, p := range []string{"old.db", "new.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", "delete", 3) } else { err = f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete") } 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("parked %s: %q %v", p, b, err) } } got, err := hostguard.Capture(f.l.DataDir, f.host) if err != nil { t.Fatal(err) } if mode == "artifacts" { if got != f.captured { t.Fatal("artifact retirement changed stamp") } if _, err := os.Stat(filepath.Join(f.l.DataDir, "new.db")); err != nil { t.Fatal(err) } } else if got.Stamp != 0 { t.Fatal("installation still active") } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil { t.Fatal(err) } if len(obs.Claim.Obligations) != 0 { t.Fatal("not acknowledged") } want := 0 if mode == "artifacts" { want = 1 } if len(obs.Claim.Inventory["box"].Paths) != want { t.Fatal("inventory mismatch") } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal("completion retry", err) } }) } } func TestLocalResumeAfterEveryRenameHoldsGuard(t *testing.T) { for _, stop := range []string{"a", "b"} { t.Run(stop, func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 5, "b": 5}) f.write(t, "a", "a") f.write(t, "b", "b") crash := errors.New("crash after rename") f.l.afterRename = func(p string) error { if p != stop { return nil } ctx, cancel := context.WithTimeout(t.Context(), 30*time.Millisecond) defer cancel() if err := f.l.Guard.With(ctx, f.host, f.captured, func(*hostclaim.Claim) error { return nil }, func() error { t.Fatal("lock released inside retirement"); return nil }); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("competing writer %v", err) } return crash } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, crash) { t.Fatal(err) } current, err := hostguard.Capture(f.l.DataDir, f.host) if err != nil || current.Stamp != 0 { t.Fatalf("not retired before rename: %+v %v", current, err) } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 1 { t.Fatal("early completion", err) } f.l.afterRename = nil if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal(err) } for _, p := range []string{"a", "b"} { if b, err := os.ReadFile(f.parked(p)); err != nil || string(b) != p { t.Fatalf("lost %s", p) } } }) } } func TestLocalRestrictiveUmask(t *testing.T) { // Umask is process-wide: this test and its fixture setup must not run in parallel. f := localFixtureFor(t, map[string]uint64{"nested/a": 5, "nested/b": 5}) f.write(t, "nested/a", "a") f.write(t, "nested/b", "b") group := os.Getegid() if os.Geteuid() == 0 { group = 65534 } for _, path := range []string{f.l.DataDir, filepath.Join(f.l.DataDir, ".parked"), filepath.Join(f.l.DataDir, "nested")} { dir, err := os.Open(path) if err != nil { t.Fatal(err) } if os.Geteuid() == 0 { if err := dir.Chown(0, group); err != nil { dir.Close() t.Fatal(err) } } if err := dir.Chmod(os.ModeSetgid | 0770); err != nil { dir.Close() t.Fatal(err) } dir.Close() } oldUmask := syscall.Umask(0o077) defer syscall.Umask(oldUmask) crash := errors.New("crash under restrictive umask") f.l.afterRename = func(string) error { return crash } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, crash) { t.Fatal(err) } base := filepath.Join(f.l.DataDir, ".parked", f.captured.Incarnation) event := filepath.Join(base, f.host, "retirement") for _, path := range []string{base, filepath.Join(base, f.host), event, filepath.Join(event, "artifacts"), filepath.Join(event, "artifacts", "nested")} { var st unix.Stat_t if err := unix.Lstat(path, &st); err != nil { t.Fatal(err) } if st.Mode&unix.S_IFMT != unix.S_IFDIR || st.Mode&07777 != 02770 || st.Gid != uint32(group) { t.Fatalf("archive directory %s: mode=%#o gid=%d", path, st.Mode, st.Gid) } } if os.Geteuid() == 0 { body, err := os.ReadFile(filepath.Join(event, ".journal.json")) if err != nil { t.Fatal(err) } dir, err := os.Open(f.l.DataDir) if err != nil { t.Fatal(err) } defer dir.Close() exe, err := os.Executable() if err != nil { t.Fatal(err) } run := func(refuse bool) { t.Helper() cmd := exec.Command(exe, "-test.run=^TestLocalRestrictiveUmaskWorker$") cmd.ExtraFiles = []*os.File{dir} cmd.Env = append(os.Environ(), "PARK_UMASK_WORKER=1", "PARK_UMASK_JOURNAL="+string(body)) if refuse { cmd.Env = append(cmd.Env, "PARK_UMASK_REFUSE=1") } if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("group worker: %v\n%s", err, out) } } // A worker must refuse, not chmod, directories owned by another UID. for _, mode := range []os.FileMode{os.ModeSetgid | 0750, 0770} { path := filepath.Join(event, "artifacts") if err := os.Chmod(path, mode); err != nil { t.Fatal(err) } run(true) st, err := os.Stat(path) if err != nil || st.Mode()&(os.ModeSetgid|0777) != mode { t.Fatal("foreign directory permissions changed", err) } if err := os.Chmod(path, os.ModeSetgid|0770); err != nil { t.Fatal(err) } } run(false) } else { t.Log("mode and group assertions passed; cross-user subprocess requires root") } f.l.afterRename = nil if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal(err) } for _, name := range []string{"a", "b"} { if body, err := os.ReadFile(f.parked("nested/" + name)); err != nil || string(body) != name { t.Fatalf("resumed artifact %s: %q %v", name, body, err) } } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 0 { t.Fatal("resumed retirement not acknowledged", err) } } func TestLocalRestrictiveUmaskWorker(t *testing.T) { if os.Getenv("PARK_UMASK_WORKER") != "1" { t.Skip("subprocess helper") } var want localJournal if err := json.Unmarshal([]byte(os.Getenv("PARK_UMASK_JOURNAL")), &want); err != nil { t.Fatal(err) } // Enter the fixture through an inherited descriptor; its temp ancestors may be private. if err := unix.Fchdir(3); err != nil { t.Fatal(err) } for _, change := range []func() error{ func() error { return syscall.Setgroups([]int{65534}) }, func() error { return syscall.Setgid(65534) }, func() error { return syscall.Setuid(65534) }, } { if err := change(); err != nil { t.Fatal(err) } } oldUmask := syscall.Umask(0o077) defer syscall.Umask(oldUmask) err := (Local{DataDir: "."}).parkLocal(t.Context(), &want) if os.Getenv("PARK_UMASK_REFUSE") == "1" { if !errors.Is(err, ErrLocalInventory) { t.Fatalf("foreign directory without required mode accepted: %v", err) } return } if err != nil { t.Fatal(err) } if _, err := os.Stat("nested/b"); !errors.Is(err, os.ErrNotExist) { t.Fatal("second process did not retire remaining artifact", err) } } func TestLocalRefusesSymlinksAndOverlaps(t *testing.T) { for _, place := range []string{"source", "ancestor", "park-root", "destination", "overlap", "missing-inventory"} { t.Run(place, func(t *testing.T) { paths := map[string]uint64{"sub/a": 5} if place == "overlap" { paths["sub"] = 3 } f := localFixtureFor(t, paths) f.write(t, "sub/a", "source") outside := t.TempDir() sentinel := filepath.Join(outside, "a") if err := os.WriteFile(sentinel, []byte("sentinel"), 0600); err != nil { t.Fatal(err) } switch place { case "source": if err := os.Remove(filepath.Join(f.l.DataDir, "sub/a")); err != nil { t.Fatal(err) } if err := os.Symlink(sentinel, filepath.Join(f.l.DataDir, "sub/a")); err != nil { t.Fatal(err) } case "ancestor": if err := os.Rename(filepath.Join(f.l.DataDir, "sub"), filepath.Join(f.l.DataDir, "safe")); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(f.l.DataDir, "sub")); err != nil { t.Fatal(err) } case "park-root": if err := os.Remove(filepath.Join(f.l.DataDir, ".parked")); err != nil { t.Fatal(err) } if err := os.Symlink(outside, filepath.Join(f.l.DataDir, ".parked")); err != nil { t.Fatal(err) } case "destination": if err := os.MkdirAll(filepath.Dir(f.parked("sub/a")), 0770); err != nil { t.Fatal(err) } if err := os.Symlink(sentinel, f.parked("sub/a")); err != nil { t.Fatal(err) } case "missing-inventory": obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil { t.Fatal(err) } if _, err := hostclaim.Update(t.Context(), f.l.Claims, obs, func(c *hostclaim.Claim) error { delete(c.Inventory, "box"); return nil }); err != nil { t.Fatal(err) } } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err == nil { t.Fatal("unsafe retirement accepted") } if b, err := os.ReadFile(sentinel); err != nil || string(b) != "sentinel" { t.Fatal("symlink target changed") } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 1 { t.Fatal("acknowledged failure", err) } }) } } func TestLocalEmptyAndAbsentArtifactsAcknowledge(t *testing.T) { for _, paths := range []map[string]uint64{{}, {"missing/file": 5}} { f := localFixtureFor(t, paths) if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal(err) } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 0 { t.Fatal("not complete", err) } } } func TestLocalOldRetirementCannotFenceSuccessor(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 5}) f.write(t, "a", "old") f.l.afterRename = func(string) error { return errors.New("crash") } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err == nil { t.Fatal("expected crash") } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil { t.Fatal(err) } if _, err := hostclaim.Update(t.Context(), f.l.Claims, obs, func(c *hostclaim.Claim) error { c.Holder.Stamp = c.Gen + 1 c.Inventory["box"].Paths["a"] = c.Gen + 1 return nil }); err != nil { t.Fatal(err) } if err := f.l.Guard.Activate(t.Context(), f.host, "box", nil); err != nil { t.Fatal(err) } f.write(t, "a", "successor") f.l.afterRename = nil if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, hostguard.ErrStampMismatch) { t.Fatal(err) } if b, err := os.ReadFile(filepath.Join(f.l.DataDir, "a")); err != nil || string(b) != "successor" { t.Fatal("successor damaged") } } func TestLocalArtifactResumeRefusesReusedSource(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 3}) f.write(t, "a", "predecessor") f.l.afterRename = func(string) error { return errors.New("crash") } if err := f.l.RetireArtifacts(t.Context(), f.host, f.captured, "local", "delete", 3); err == nil { t.Fatal("expected crash") } f.write(t, "a", "replacement") f.l.afterRename = nil if err := f.l.RetireArtifacts(t.Context(), f.host, f.captured, "local", "delete", 3); !errors.Is(err, ErrLocalJournal) { t.Fatal(err) } if b, err := os.ReadFile(filepath.Join(f.l.DataDir, "a")); err != nil || string(b) != "replacement" { t.Fatal("replacement changed") } if b, err := os.ReadFile(f.parked("a")); err != nil || string(b) != "predecessor" { t.Fatal("archive changed") } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 1 || len(obs.Claim.Inventory["box"].Paths) != 1 { t.Fatal("source reuse acknowledged", err) } } func TestLocalMissingPathRecreatedAfterCrash(t *testing.T) { for _, kind := range []string{"file", "directory", "leaf-symlink", "ancestor-symlink", "still-missing"} { t.Run(kind, func(t *testing.T) { missing := "a-missing" if kind == "ancestor-symlink" { missing += "/child" } f := localFixtureFor(t, map[string]uint64{missing: 5, "z-present": 5}) f.write(t, "z-present", "old") crash := errors.New("crash after missing check and artifact rename") f.l.afterRename = func(string) error { return crash } if err := f.l.RetireArtifacts(t.Context(), f.host, f.captured, "local", "delete", 5); !errors.Is(err, crash) { t.Fatal(err) } f.l.afterRename = nil outside := t.TempDir() target := filepath.Join(outside, "child") if err := os.WriteFile(target, []byte("outside"), 0600); err != nil { t.Fatal(err) } path := filepath.Join(f.l.DataDir, "a-missing") switch kind { case "file": f.write(t, missing, "new") case "directory": if err := os.Mkdir(path, 0770); err != nil { t.Fatal(err) } case "leaf-symlink": if err := os.Symlink(target, path); err != nil { t.Fatal(err) } case "ancestor-symlink": if err := os.Symlink(outside, path); err != nil { t.Fatal(err) } } err := f.l.RetireArtifacts(t.Context(), f.host, f.captured, "local", "delete", 5) if kind == "still-missing" { if err != nil { t.Fatal(err) } } else if err == nil { t.Fatal("acknowledged a recreated missing path") } obs, readErr := hostclaim.Read(t.Context(), f.l.Claims, f.host) if readErr != nil { t.Fatal(readErr) } if kind == "still-missing" { if len(obs.Claim.Obligations) != 0 || len(obs.Claim.Inventory["box"].Paths) != 0 { t.Fatal("missing path did not acknowledge") } } else { if len(obs.Claim.Obligations) != 1 || len(obs.Claim.Inventory["box"].Paths) != 2 { t.Fatal("refusal changed inventory or chore") } if _, err := os.Lstat(path); err != nil { t.Fatal("recreated artifact lost", err) } } if got, err := hostguard.Capture(f.l.DataDir, f.host); err != nil || got != f.captured { t.Fatal("artifact retirement changed installation", err) } if b, err := os.ReadFile(target); err != nil || string(b) != "outside" { t.Fatal("followed planted symlink", err) } if b, err := os.ReadFile(f.parked("z-present")); err != nil || string(b) != "old" { t.Fatal("lost parked artifact", err) } }) } } func TestLocalResolvedDirectoryAndDestinationConflict(t *testing.T) { t.Run("nested directory", func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"nested/instance": 5}) f.write(t, "nested/instance/data/db", "database") if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal(err) } if b, err := os.ReadFile(f.parked("nested/instance/data/db")); err != nil || string(b) != "database" { t.Fatal("nested tree lost", err) } }) t.Run("destination conflict", func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 5}) f.write(t, "a", "source") if err := os.MkdirAll(filepath.Dir(f.parked("a")), 0770); err != nil { t.Fatal(err) } if err := os.WriteFile(f.parked("a"), []byte("foreign"), 0660); err != nil { t.Fatal(err) } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, ErrLocalJournal) { t.Fatal(err) } if b, err := os.ReadFile(f.parked("a")); err != nil || string(b) != "foreign" { t.Fatal("destination overwritten") } if b, err := os.ReadFile(filepath.Join(f.l.DataDir, "a")); err != nil || string(b) != "source" { t.Fatal("source lost") } }) } func TestLocalAuthorizationAndInventoryEnrollment(t *testing.T) { t.Run("authorization", func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 5}) f.write(t, "a", "data") denied := errors.New("stop evidence unavailable") f.l.Authorize = func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return denied } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, denied) { t.Fatal(err) } got, err := hostguard.Capture(f.l.DataDir, f.host) if err != nil || got != f.captured { t.Fatal("unauthorized stamp mutation", err) } if _, err := os.Stat(filepath.Join(f.l.DataDir, "a")); err != nil { t.Fatal(err) } }) t.Run("enrollment", func(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"old": 3}) allow := func(*hostclaim.Claim) error { return nil } if err := f.l.RecordInventory(t.Context(), f.host, f.captured, "flat", []string{"new"}, allow); err != nil { t.Fatal(err) } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil { t.Fatal(err) } if obs.Claim.Inventory["box"].Paths["old"] != 3 || obs.Claim.Inventory["box"].Paths["new"] != 5 { t.Fatal("inventory was replaced") } if _, err := os.Stat(filepath.Join(f.l.DataDir, "new")); !errors.Is(err, os.ErrNotExist) { t.Fatal("enrollment materialized state") } for _, p := range []string{"old", "old/child", "new/child", "../escape", "hostguard/record", ".parked/file"} { if err := f.l.RecordInventory(t.Context(), f.host, f.captured, "flat", []string{p}, allow); !errors.Is(err, ErrLocalInventory) { t.Fatalf("%s: %v", p, err) } } if err := f.l.RecordInventory(t.Context(), f.host, f.captured, "nested", nil, allow); !errors.Is(err, ErrLocalInventory) { t.Fatal("changed populated layout", err) } }) } func TestLocalCompletionRechecksAuthority(t *testing.T) { f := localFixtureFor(t, map[string]uint64{"a": 5}) f.write(t, "a", "data") denied := errors.New("fence lost before acknowledgement") calls := 0 f.l.Authorize = func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { calls++ if calls > 1 { return denied } return nil } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); !errors.Is(err, denied) { t.Fatal(err) } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 1 { t.Fatal("completed without authority", err) } f.l.Authorize = func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil } if err := f.l.RetireInstallation(t.Context(), f.host, f.captured, "local", "delete"); err != nil { t.Fatal(err) } } func TestLocalUntouchedBoxAcknowledgement(t *testing.T) { for _, kind := range []string{"absent-record", "active-record", "corrupt-record", "nonempty-inventory"} { t.Run(kind, func(t *testing.T) { paths := map[string]uint64{} if kind == "nonempty-inventory" { paths["a"] = 5 } f := localFixtureFor(t, paths) record := filepath.Join(f.l.DataDir, "hostguard", f.host+".json") if kind == "corrupt-record" { if err := os.WriteFile(record, []byte("{"), 0660); err != nil { t.Fatal(err) } } else if kind != "active-record" { if err := os.Remove(record); err != nil { t.Fatal(err) } } err := f.l.AcknowledgeEmpty(t.Context(), f.host, f.captured.Incarnation, "local", "delete") if kind == "absent-record" { if err != nil { t.Fatal(err) } obs, err := hostclaim.Read(t.Context(), f.l.Claims, f.host) if err != nil || len(obs.Claim.Obligations) != 0 { t.Fatal("empty box not acknowledged", err) } if _, err := os.Stat(record); !errors.Is(err, os.ErrNotExist) { t.Fatal("empty retirement created stamp", err) } } else if err == nil { t.Fatal("unsafe empty acknowledgement") } }) } } // 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////artifacts/, and its journal names // the move's op. That is park.Local's own contract; how F3 selects a // move's local retirements by op across both receipts (this journal and // the moves-owned rename's) is internal/host's // TestMoveLocalRetirementsAreFindableByOp. 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) } }) } } }