carlos / platform Public

hostops: the recovery test exercises fresh claim reads and dispatch (Plan 5 review)

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 075c50deacab0567c43bc81c7ef0618922cae482 parent e5ff37a
1 file changed, +83 −7
  • internal/hostops/operations_test.go +83 −7

CI passed — run details

diff --git a/internal/hostops/operations_test.go b/internal/hostops/operations_test.go
index 0732fd7f7..e9526e34c 100644
--- a/internal/hostops/operations_test.go
+++ b/internal/hostops/operations_test.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
+ "reflect"
"testing"
"time"
@@ -32,11 +33,20 @@ func (a *authority) PurgeRetirement(context.Context, hostclaim.Observed) error {
type faultStore struct {
release.Store
failGet string
+ afterList func(string, []string) error
beforePut func(string) error
beforeDelete func(string, string) error
bareDeletes int
}
+func (s *faultStore) List(ctx context.Context, prefix string) ([]string, error) {
+ keys, err := s.Store.List(ctx, prefix)
+ if err == nil && s.afterList != nil {
+ err = s.afterList(prefix, keys)
+ }
+ return keys, err
+}
+
func (s *faultStore) Get(ctx context.Context, key string) ([]byte, string, error) {
if key == s.failGet {
return nil, "", errors.New("read failed")
@@ -337,13 +347,79 @@ func TestRecoveryFreshReadsAndRequiredHooks(t *testing.T) {
if err := (Recovery{Controller: f.c}).Pass(f.ctx); !errors.Is(err, ErrContract) {
t.Fatal(err)
}
- calls := 0
- r := Recovery{Controller: f.c, Purge: func(context.Context, hostclaim.Observed) error { return nil }, Placement: func(context.Context, hostclaim.Observed) error { return nil }, SweepRecords: func(context.Context) error { calls++; return nil }, SweepPurges: func(context.Context) error { calls++; return nil }, SweepAdmissions: func(context.Context) error { return nil }}
- if err := r.Pass(f.ctx); err != nil {
- t.Fatal(err)
- }
- if calls != 2 {
- t.Fatal(calls)
+ for _, tc := range []struct {
+ name string
+ replace bool
+ wantCalls int
+ }{
+ {name: "advanced purge", replace: true, wantCalls: 1},
+ {name: "released purge", wantCalls: 0},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ f := newFixture(t)
+ host := "shop.bab.example.com"
+ before, err := hostclaim.Read(f.ctx, f.store, host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ before, err = hostclaim.Update(f.ctx, f.store, before, FencePurge("purge-old", f.now))
+ if err != nil {
+ t.Fatal(err)
+ }
+ startedAt := f.now
+ f.now = f.now.Add(6 * time.Minute)
+ key, err := hostclaim.Key(host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var fresh hostclaim.Observed
+ listCalls := 0
+ f.store.afterList = func(prefix string, keys []string) error {
+ listCalls++
+ if prefix != "control/hosts/" || len(keys) != 1 || keys[0] != key {
+ t.Fatalf("discovery = %q %v, want the populated claim", prefix, keys)
+ }
+ var err error
+ fresh, err = hostclaim.Update(f.ctx, f.store.Store, before, ReleasePurgeFence("purge-old"))
+ if err == nil && tc.replace {
+ fresh, err = hostclaim.Update(f.ctx, f.store.Store, fresh, FencePurge("purge-new", startedAt))
+ }
+ return err
+ }
+ purgeCalls, recordSweeps, purgeSweeps, admissionSweeps := 0, 0, 0, 0
+ r := Recovery{
+ Controller: f.c,
+ Purge: func(_ context.Context, got hostclaim.Observed) error {
+ purgeCalls++
+ if !tc.replace {
+ t.Fatal("dispatched a purge released after discovery")
+ }
+ if !reflect.DeepEqual(got, fresh) {
+ t.Fatalf("dispatched stale claim: gen=%d op=%q etag=%q; want gen=%d op=%q etag=%q", got.Claim.Gen, got.Claim.Op.ID, got.ETag, fresh.Claim.Gen, fresh.Claim.Op.ID, fresh.ETag)
+ }
+ return nil
+ },
+ Placement: func(context.Context, hostclaim.Observed) error {
+ t.Fatal("dispatched placement for a purge claim")
+ return nil
+ },
+ SweepRecords: func(context.Context) error { recordSweeps++; return nil },
+ SweepPurges: func(context.Context) error { purgeSweeps++; return nil },
+ SweepAdmissions: func(context.Context) error { admissionSweeps++; return nil },
+ }
+ if err := r.Pass(f.ctx); err != nil {
+ t.Fatal(err)
+ }
+ if listCalls != 1 || fresh.Claim.Gen <= before.Claim.Gen || fresh.ETag == before.ETag {
+ t.Fatal("claim did not advance between discovery and recovery")
+ }
+ if purgeCalls != tc.wantCalls {
+ t.Fatalf("purge calls = %d, want %d", purgeCalls, tc.wantCalls)
+ }
+ if recordSweeps != 1 || purgeSweeps != 1 || admissionSweeps != 1 {
+ t.Fatalf("sweeps: records=%d purges=%d admissions=%d; want one each", recordSweeps, purgeSweeps, admissionSweeps)
+ }
+ })
}
}