package hostclaim import ( "bytes" "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func newHolder() NewHolder { return NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json", Account: "bab", App: "jam", Host: testHost, Region: "ie", RecordRegion: "ie", CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)} } var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) func TestCreateLifecycle(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, _ := Read(ctx, s, testHost) o, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now)) if err != nil || o.Claim.Op.Kind != KindCreating || o.Claim.Op.Intent["record"] != string(record) { t.Fatalf("begin = %+v, %v", o, err) } if o.Claim.Holder.Location != "instances/"+testHost+"/"+o.Claim.Holder.Incarnation { t.Fatalf("location = %q", o.Claim.Holder.Location) } o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")) if err != nil || o.Claim.Op.Kind != KindServing || o.Claim.Op.Exec != "adi-knitting" || o.Claim.Holder.Stamp != o.Claim.Gen { t.Fatalf("complete = %+v, %v", o.Claim, err) } if len(o.Claim.Touched.Boxes) != 1 || o.Claim.Touched.Boxes[0] != "adi-knitting" { t.Fatalf("executor not recorded as touched: %+v", o.Claim.Touched) } if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrStaleOp) { t.Fatalf("replay = %v, want ErrStaleOp", err) } if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c2", "console", now)); !errors.Is(err, ErrConflict) { t.Fatalf("stale create = %v, want ErrConflict", err) } } func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, _ := Read(ctx, s, testHost) other := newHolder() other.Host = "other.bab.oncarlos.com" if _, err := Update(ctx, s, reg, BeginCreate(other, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("another host's create on this claim = %v", err) } badKey := newHolder() badKey.Key = "apps/zzz/jam/instances/" + testHost + ".json" if _, err := Update(ctx, s, reg, BeginCreate(badKey, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("mismatched record key = %v", err) } other2 := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T09:00:00Z"}`) if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), other2, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("record body with another CreatedAt = %v", err) } if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), []byte("not json"), "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("record body not JSON = %v", err) } wrongRegion := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"uk","created_at":"2026-10-04T01:00:00Z"}`) if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), wrongRegion, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("record body in another region = %v", err) } // A sidecar key cannot claim an instance-shaped host (P1, plan review round 2). sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/shop.json", Account: "bab", App: "jam", Name: "shop", Host: testHost, Region: "ie", RecordRegion: "ie", CreatedAt: newHolder().CreatedAt} if _, err := Update(ctx, s, reg, BeginCreate(sc, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("sidecar key on a non-sidecar host = %v", err) } } func TestBeginCreateSidecar(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) host := "bot.jam.bab.sidecar.internal" sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/bot.json", Account: "bab", App: "jam", Name: "bot", Host: host, Region: "ie", RecordRegion: "ie", CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)} body := []byte(`{"name":"bot","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) reg, _ := Read(ctx, s, host) if _, err := Update(ctx, s, reg, BeginCreate(sc, body, "op_s1", "console", now)); err != nil { t.Fatalf("sidecar create = %v", err) } s2 := release.NewDirStore(t.TempDir()) reg2, _ := Read(ctx, s2, host) wrongName := []byte(`{"name":"other","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`) if _, err := Update(ctx, s2, reg2, BeginCreate(sc, wrongName, "op_s1", "console", now)); !errors.Is(err, ErrWrongState) { t.Fatalf("sidecar body with another name = %v", err) } } func TestCancelRetiresAndRefusesReuse(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, _ := Read(ctx, s, testHost) o, _ := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now)) inc := o.Claim.Holder.Incarnation o, err := Update(ctx, s, o, CancelCreate("op_c1", now)) if err != nil || !o.Claim.Free() || len(o.Claim.CancelledCreates) != 1 || o.Claim.RetiredIncarnations[0] != inc { t.Fatalf("cancel = %+v, %v", o.Claim, err) } if _, err := Update(ctx, s, o, BeginCreate(newHolder(), record, "op_c3", "console", now)); !errors.Is(err, ErrRetiredIncarnation) { t.Fatalf("reuse = %v, want ErrRetiredIncarnation", err) } } func TestCancelCreateRefusesExecutionAuthority(t *testing.T) { for _, tc := range []struct { name string exec string participants []string stamp uint64 }{ {name: "executor", exec: "adi-knitting"}, {name: "participants", participants: []string{"adi-knitting"}}, {name: "stamp", stamp: 1}, {name: "all", exec: "adi-knitting", participants: []string{"adi-knitting"}, stamp: 1}, } { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } o, err := Update(ctx, s, reg, func(c *Claim) error { if err := BeginCreate(newHolder(), record, "op_c1", "console", now)(c); err != nil { return err } c.Op.Exec = tc.exec c.Op.Participants = tc.participants c.Holder.Stamp = tc.stamp return nil }) if err != nil { t.Fatalf("store creating claim with authority: %v", err) } key, err := Key(testHost) if err != nil { t.Fatal(err) } before, beforeETag, err := s.Get(ctx, key) if err != nil { t.Fatal(err) } if _, err := Update(ctx, s, o, CancelCreate("op_c1", now)); !errors.Is(err, ErrWrongState) { t.Errorf("cancel with authority = %v, want ErrWrongState", err) } after, afterETag, err := s.Get(ctx, key) if err != nil { t.Fatal(err) } if !bytes.Equal(before, after) || beforeETag != afterETag { t.Errorf("cancel changed stored claim: before ETag %q, after ETag %q", beforeETag, afterETag) } }) } } func TestCreateHomeRegionParksEffectiveRegion(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) nh := newHolder() nh.RecordRegion = "" body := bytes.Replace(record, []byte(`"region":"ie"`), []byte(`"region":""`), 1) obs, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } o, err := Update(ctx, s, obs, BeginCreate(nh, body, "op_c", "console", time.Now())) if err != nil { t.Fatal(err) } if len(o.Claim.Touched.Regions) != 1 || o.Claim.Touched.Regions[0] != "ie" || o.Claim.Op.Intent["record"] != string(body) { t.Fatalf("home-region create = %+v", o.Claim) } o, err = Update(ctx, s, o, CompleteCreate("op_c", "B0")) if err != nil { t.Fatal(err) } o, err = Update(ctx, s, o, BeginDelete("op_d", "console", `"e"`, time.Now(), nil, nil, "")) if err != nil { t.Fatal(err) } o, err = Update(ctx, s, o, ToParking("op_d", nil, nil, counter())) if err != nil { t.Fatal(err) } assertParkingTargets(t, o.Claim.Obligations, []Obligation{ {ParkTarget: "instances/" + testHost + "/" + o.Claim.Holder.Incarnation, Op: "op_d", Kind: "park-replica", Region: "ie", Owner: "region:ie"}, {Op: "op_d", Kind: "park-local", Box: "B0", Owner: "box:B0"}, }) } func TestBeginCreateRefusesEmptyEffectiveRegion(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) nh := newHolder() nh.Region, nh.RecordRegion = "", "" body := bytes.Replace(record, []byte(`"region":"ie"`), []byte(`"region":""`), 1) obs, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } if _, err := Update(ctx, s, obs, BeginCreate(nh, body, "op_c", "console", time.Now())); !errors.Is(err, ErrWrongState) { t.Errorf("empty effective region = %v, want ErrWrongState", err) } after, err := Read(ctx, s, testHost) if err != nil || !after.Absent { t.Fatalf("read after refused create = %+v, %v; want absent", after, err) } } func TestCompleteCreateRefusesExecutionAuthority(t *testing.T) { for _, tc := range []struct { name string exec string participants []string stamp uint64 }{ {name: "executor", exec: "adi-knitting"}, {name: "participants", participants: []string{"adi-knitting"}}, {name: "stamp", stamp: 1}, {name: "all", exec: "adi-knitting", participants: []string{"adi-knitting"}, stamp: 1}, } { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } o, err := Update(ctx, s, reg, func(c *Claim) error { if err := BeginCreate(newHolder(), record, "op_c1", "console", now)(c); err != nil { return err } c.Op.Exec = tc.exec c.Op.Participants = tc.participants c.Holder.Stamp = tc.stamp return nil }) if err != nil { t.Fatalf("store creating claim with authority: %v", err) } key, err := Key(testHost) if err != nil { t.Fatal(err) } before, beforeETag, err := s.Get(ctx, key) if err != nil { t.Fatal(err) } if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "new-executor")); !errors.Is(err, ErrWrongState) { t.Errorf("complete with authority = %v, want ErrWrongState", err) } after, afterETag, err := s.Get(ctx, key) if err != nil { t.Fatal(err) } if !bytes.Equal(before, after) || beforeETag != afterETag { t.Errorf("complete changed stored claim: before ETag %q, after ETag %q", beforeETag, afterETag) } }) } } func TestCreateCompletesOnlyOnTheSelectedExecutor(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC) reg, _ := Read(ctx, s, testHost) sel := ExecutorSelection{Box: "bri-knitting", Enrollment: "enrollment-b"} if _, err := Update(ctx, s, reg, BeginCreateOn(newHolder(), record, "op_c1", "console", now, ExecutorSelection{})); !errors.Is(err, ErrMalformed) { t.Fatalf("create with no selection: %v", err) } o, err := Update(ctx, s, reg, BeginCreateOn(newHolder(), record, "op_c1", "console", now, sel)) if err != nil { t.Fatal(err) } if o.Claim.Op.Intent["selected_executor"] != sel.Box || o.Claim.Op.Intent["executor_enrollment"] != sel.Enrollment || o.Claim.Op.Exec != "" { t.Fatalf("intent: %+v", o.Claim.Op) } if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrWrongState) { t.Fatalf("completion substituted a box: %v", err) } if o, err = Update(ctx, s, o, CompleteCreate("op_c1", sel.Box)); err != nil || o.Claim.Op.Exec != sel.Box { t.Fatalf("complete: %v %+v", err, o.Claim.Op) } }