package hostclaim import ( "context" "encoding/json" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func TestPlan11AutomaticEvidenceRetainsAndStrictDecodes(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) fence := FenceRef{Enrollment: strings.Repeat("a", 32), Key: "control/box-fences/" + strings.Repeat("a", 32) + ".json", SHA256: strings.Repeat("b", 64)} loss := AutomaticLoss{Box: "box-a", Enrollment: fence.Enrollment, Fence: fence, ObservedAt: "2026-10-09T10:00:00Z", Op: "reloc", Kind: KindRelocating, Phase: "releasing", Role: "source", Incarnation: o.Claim.Holder.Incarnation, Stamp: 1, Timers: TimersUnknown} loss.ID = automaticLossID(loss.Op, loss.Box, fence) o, err := Update(ctx, s, o, func(c *Claim) error { c.EvidenceRefs = []AutomaticLoss{loss} c.LossRefs = []string{loss.ID} return nil }) if err != nil { t.Fatal(err) } o, err = Update(ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindParking, ID: "retire"} return FreeAfterPark("retire")(c) }) if err != nil { t.Fatal(err) } read, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } if len(read.Claim.EvidenceRefs) != 1 || read.Claim.EvidenceRefs[0].ID != loss.ID { t.Fatal("free discarded loss") } nh := newHolder() nh.CreatedAt = nh.CreatedAt.Add(time.Hour) successorRecord, _ := json.Marshal(map[string]any{"host": nh.Host, "account": nh.Account, "app": nh.App, "region": nh.RecordRegion, "created_at": nh.CreatedAt}) o, err = Update(ctx, s, o, BeginCreate(nh, successorRecord, "successor", "console", nh.CreatedAt)) if err != nil || len(o.Claim.EvidenceRefs) != 1 || o.Claim.EvidenceRefs[0].Incarnation != loss.Incarnation { t.Fatal("successor discarded prior loss", err) } read = o for name, mutate := range map[string]func(*AutomaticLoss){ "time": func(l *AutomaticLoss) { l.ObservedAt = "bad" }, "identity": func(l *AutomaticLoss) { l.ID = "auto-" + strings.Repeat("c", 32) }, "enrollment": func(l *AutomaticLoss) { l.Enrollment = strings.Repeat("c", 32) }, "role": func(l *AutomaticLoss) { l.Role = "observer" }, } { _, err := Update(ctx, s, o, func(c *Claim) error { mutate(&c.EvidenceRefs[0]); return nil }) if err == nil { t.Fatal("malformed loss accepted", name) } } raw, _ := json.Marshal(read.Claim) raw = []byte(strings.Replace(string(raw), `"observed_at":`, `"unknown":true,"observed_at":`, 1)) key, _ := Key(testHost) if _, err := s.Put(ctx, key, raw, read.ETag); err != nil { t.Fatal(err) } if _, err := Read(ctx, s, testHost); err == nil { t.Fatal("unknown evidence field accepted") } } func TestPlan11RelocationReleaseIsStopOnly(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) ob := Obligation{ID: "relocation-release", Op: "reloc", Kind: "relocation-release", Region: "ie", Box: "box-a", Owner: "region:ie", Retirement: "installation-1"} next, err := Update(ctx, s, o, func(c *Claim) error { c.Obligations = []Obligation{ob}; return nil }) if err != nil { t.Fatal(err) } for name, mut := range map[string]func(*Obligation){ "park": func(o *Obligation) { o.Park = &ParkScope{Location: "instances/" + testHost, Incarnation: next.Claim.Holder.Incarnation, EvidenceAt: time.Now(), RequestLimit: time.Second} }, "owner": func(o *Obligation) { o.Owner = "box:box-a" }, "box": func(o *Obligation) { o.Box = "" }, "generation": func(o *Obligation) { o.Generation = "g" }, } { _, err := Update(ctx, s, next, func(c *Claim) error { mut(&c.Obligations[0]); return nil }) if err == nil { t.Fatal("invalid release accepted", name) } } } func TestPlan11QueueReleaseIsInstallationBound(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o, err := Update(ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindRelocating, ID: "reloc", Phase: "releasing", Exec: "box-a", Intent: map[string]string{"source_box": "box-a", "region": "ie"}} return QueueRelocationRelease("reloc", "box-a", "ie", "install-1")(c) }) if err != nil { t.Fatal(err) } if len(o.Claim.Obligations) != 1 || o.Claim.Obligations[0].Owner != "region:ie" { t.Fatal("no stop-only chore") } if err := QueueRelocationRelease("reloc", "box-a", "ie", "install-1")(o.Claim); err != nil { t.Fatal(err) } if len(o.Claim.Obligations) != 1 { t.Fatal("retry duplicated release") } if err := QueueRelocationRelease("reloc", "box-a", "ie", "different-install")(o.Claim); err == nil { t.Fatal("rebound release") } if err := QueueRelocationRelease("reloc", "box-b", "ie", "install-1")(o.Claim); err == nil { t.Fatal("wrong executor") } }