//go:build unix package hostguard import ( "bytes" "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" ) // B3 (roster binding): an obsolete cold handover barrier is settled without // a claim session advance (the live successor admission still holds the // claim), under the host lock, only for a prior with a completed receipt. // Settlement undoes BeginColdHandover (the deny slot back to the prior's // completed fence), retires the successor's stamp so it is never installed, // runs record after the guard write, and is idempotent. func TestSettleObsoleteColdHandover(t *testing.T) { f, prior, captured := fenceFixture(t) f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" store := &rootTestStore{dir: t.TempDir()} f.g.Root = NewRootAuthority(store) in := RootInstallation{Host: prior.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: prior.Box, Enrollment: prior.Enrollment, Migration: 1} if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { t.Fatal(err) } f.obs.Claim.Op.Kind = hostclaim.KindMoving if _, err := f.g.BeginFence(t.Context(), prior); err != nil { t.Fatal(err) } if err := f.g.CompleteFence(t.Context(), prior, []byte("receipt"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { t.Fatal(err) } barrier := prior barrier.FenceID, barrier.Stamp, barrier.Session, barrier.DirectoryRevision = "barrier", 17, 17, "obsolete-binding" f.obs.Claim.Holder.Stamp, f.obs.Claim.Holder.Session = 17, 17 if err := f.g.BeginColdHandover(t.Context(), prior, barrier, allow); err != nil { t.Fatal(err) } if err := f.g.AbandonColdHandover(t.Context(), barrier); !errors.Is(err, ErrFenced) { t.Fatal("the session-advance abandonment applies here:", err) } records := 0 record := func() error { records++; return nil } other := barrier other.FenceID = "other" if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, other, record); !errors.Is(err, ErrFenceTarget) { t.Fatalf("another barrier: %v", err) } unfenced := prior unfenced.FenceID = "never-completed" if err := f.g.SettleObsoleteColdHandover(t.Context(), unfenced, barrier, record); !errors.Is(err, ErrNoFenceReceipt) { t.Fatalf("a prior without a completed receipt: %v", err) } if records != 0 { t.Fatal("a refused settlement recorded") } // A failed record leaves the guard settled; the retry only records. failed := errors.New("journal write failed") if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, barrier, func() error { return failed }); !errors.Is(err, failed) { t.Fatal(err) } r, err := f.g.Root.read(prior.Host) if err != nil || r.barrier != nil || r.ActiveFence == nil || *r.ActiveFence != prior || r.ActiveStamp != prior.Stamp || len(r.FenceJournal) != 1 || r.FenceJournal[0].Target != prior { t.Fatalf("after settlement: %+v %v", r, err) } if len(r.rootRetired) != 1 || r.rootRetired[0] != (Captured{Incarnation: barrier.Incarnation, Stamp: barrier.Stamp}) { t.Fatalf("successor stamp not retired: %v", r.rootRetired) } before, _ := store.Read(prior.Host + ".json") if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, barrier, record); err != nil || records != 1 { t.Fatalf("idempotent settlement: %v, records %d", err, records) } if after, _ := store.Read(prior.Host + ".json"); !bytes.Equal(before, after) { t.Fatal("a repeated settlement rewrote the record") } // The successor is never installed; the prior stays cleanable. successor := in successor.Stamp = barrier.Stamp if err := f.g.EstablishRoot(t.Context(), successor, allow); !errors.Is(err, ErrRetiredStamp) { t.Fatalf("settled successor installed: %v", err) } if err := f.g.CleanupWith(t.Context(), prior, func(context.Context, FenceReceipt) error { return nil }, func() error { return nil }); err != nil { t.Fatal("prior cleanup after settlement:", err) } }