//go:build unix package hostbox import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // N4: a relocation target's held row is minted through the same typed // inventory as reconcile's own (ownedRow): on an empty installation the // census initialises, the route entry is recorded and materialized, and // the row is held by the op with its ClaimOwnership bound to the captured // stamp. A second call is idempotent. Another incarnation and a flat route // refuse with ErrChanged; a host-named file under the data root refuses // with ErrTypedInventory before any row is written. func TestMintHeldBindsTheRowToTheInstallStamp(t *testing.T) { type mint struct { f admissionFixture reg *registry.Registry inv *StopInventoryDB adapter ReconcileAdapter rec provision.ClaimRecord captured hostguard.Captured } setup := func(t *testing.T) mint { t.Helper() f := newAdmissionFixture(t, false) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) inv, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { inv.Close() }) launches, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { launches.Close() }) census := &EmptyInstallationCensus{Inventory: inv, Registry: reg, Launches: launches, Box: "box", Enrollment: "en", DirectoryRevision: "1", Roots: map[string]string{"data": f.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {"hostguard"}}} adapter := ReconcileAdapter{Registry: reg, Inventory: inv, Census: census, Box: "box", Enrollment: "en", DirectoryRevision: "1", Reconciler: Reconciler{Claims: f.store, Box: "box", Region: "ie", Guard: hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}}}} record := provision.Record{Schema: 1, Host: f.host, Slug: "shop", App: "app", Account: f.account, Region: "ie", Channel: "stable", CreatedAt: time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)} rec := provision.ClaimRecord{Host: f.host, Key: f.key, Family: "instance", Account: f.account, App: "app", CreatedAt: record.CreatedAt, Value: record, Route: provision.RouteFor(record, f.data, "", true, nil)} return mint{f: f, reg: reg, inv: inv, adapter: adapter, rec: rec, captured: hostguard.Captured{Incarnation: f.holder.Incarnation, Stamp: f.holder.Stamp}} } ctx := t.Context() t.Run("from an empty installation", func(t *testing.T) { m := setup(t) if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); err != nil { t.Fatal(err) } row, err := m.reg.Get(m.f.host) if err != nil || row == nil { t.Fatalf("row: %+v %v", row, err) } sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", m.f.host, m.captured.Incarnation, m.captured.Stamp, "box"))) if row.Status != "relocating" || row.HoldOp != "op-1" || row.ClaimOwnership.ClaimStamp != m.captured.Stamp || row.ClaimOwnership.ClaimOwner != hex.EncodeToString(sum[:]) || row.ClaimOwnership.ClaimIncarnation != m.captured.Incarnation || !provision.NestedPaths(m.f.data, *row) { t.Fatalf("row %+v", row) } in, err := m.inv.ReadInstallation(ctx, m.f.host, m.captured.Incarnation, m.captured.Stamp) if err != nil || !in.Complete { t.Fatalf("inventory: %+v %v", in, err) } materialized := 0 for _, e := range in.Entries { if e.Kind == "route" && e.RelativePath == m.f.host && e.Materialization == "materialized" && e.RowIdentity == row.ClaimOwnership.ClaimRowID { materialized++ } } if materialized != 1 { t.Fatalf("route entries: %+v", in.Entries) } if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); err != nil { t.Fatalf("a second mint: %v", err) } again, err := m.reg.Get(m.f.host) if err != nil || *again != *row { t.Fatalf("the second mint changed the row: %+v %v", again, err) } if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-2"); !errors.Is(err, registry.ErrClaimOwnership) { t.Fatalf("another op's mint over a held row: %v", err) } }) t.Run("another incarnation", func(t *testing.T) { m := setup(t) m.captured.Incarnation = "other" if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrChanged) { t.Fatalf("got %v", err) } }) t.Run("a flat route", func(t *testing.T) { m := setup(t) m.rec.Route = provision.RouteFor(m.rec.Value.(provision.Record), m.f.data, "", false, nil) if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrChanged) { t.Fatalf("got %v", err) } }) t.Run("not a hold status", func(t *testing.T) { m := setup(t) if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "ready", "op-1"); !errors.Is(err, ErrUnsupported) { t.Fatalf("got %v", err) } }) t.Run("a host-named file under the data root", func(t *testing.T) { m := setup(t) if err := os.WriteFile(filepath.Join(m.f.data, m.f.host+".env"), []byte("X=1\n"), 0o600); err != nil { t.Fatal(err) } if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrTypedInventory) { t.Fatalf("got %v", err) } if row, err := m.reg.Get(m.f.host); err != nil || row != nil { t.Fatalf("a row was written: %+v %v", row, err) } }) }