package hostclaim import ( "context" "errors" "reflect" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" ) type retainedLossStore struct { release.Store after bool } func (s retainedLossStore) Put(ctx context.Context, key string, body []byte, match string) (string, error) { if s.after { if _, err := s.Store.Put(ctx, key, body, match); err != nil { return "", err } } return "", release.ErrCASConflict } func TestNativeAdmissionLossSurvivesRetirement(t *testing.T) { for _, failure := range []string{"none", "before-CAS", "lost-reply"} { t.Run(failure, func(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) for _, generation := range []string{"first", "second"} { h := o.Claim.Holder var err error o, err = Update(ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: "adi-knitting", Mode: "cold", NewGeneration: generation, ObservedSession: h.Session, ObservedGeneration: h.Generation, RemoteMaxTXID: 42})) if err != nil { t.Fatal(err) } } h := o.Claim.Holder want := RetainedAdmissionLoss{Incarnation: h.Incarnation, Location: h.Location, Account: h.Account, App: h.App, Admission: h.Admissions[1]} if want.Admission.WritesLost == nil { t.Fatal("cold wake did not declare predecessor loss") } var err error o, err = Update(ctx, s, o, BeginDelete("delete", "test", "etag", time.Now(), nil, nil, "")) if err != nil { t.Fatal(err) } o, err = Update(ctx, s, o, ToParking("delete", nil, nil, counter())) if err != nil { t.Fatal(err) } if _, err = Update(ctx, s, o, FreeAfterPark("delete")); !errors.Is(err, ErrWrongState) { t.Fatalf("outstanding parks: %v", err) } for _, ob := range o.Claim.Obligations { o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)) if err != nil { t.Fatal(err) } } if failure != "none" { _, err = Update(ctx, retainedLossStore{s, failure == "lost-reply"}, o, FreeAfterPark("delete")) if !errors.Is(err, ErrConflict) { t.Fatalf("fault: %v", err) } o, err = Read(ctx, s, testHost) if err != nil { t.Fatal(err) } if failure == "before-CAS" && (o.Claim.Holder == nil || len(o.Claim.RetainedAdmissionLosses) != 0) { t.Fatal("failed CAS changed evidence") } } if !o.Claim.Free() { o, err = Update(ctx, s, o, FreeAfterPark("delete")) if err != nil { t.Fatal(err) } } o, err = Read(ctx, s, testHost) if err != nil || !o.Claim.Free() || !reflect.DeepEqual(o.Claim.RetainedAdmissionLosses, []RetainedAdmissionLoss{want}) { t.Fatalf("retained loss: %+v, %v", o.Claim, err) } for i := 0; i < retiredCap+1; i++ { holder := newHolder() holder.CreatedAt = holder.CreatedAt.Add(time.Duration(i+1) * time.Hour) o, err = Update(ctx, s, o, BeginCreate(holder, []byte(strings.Replace(string(record), "2026-10-04T01:00:00Z", holder.CreatedAt.Format(time.RFC3339), 1)), "create", "test", time.Now())) if err != nil { t.Fatal(err) } o, err = Update(ctx, s, o, CancelCreate("create", time.Now())) if err != nil { t.Fatal(err) } } if !reflect.DeepEqual(o.Claim.RetainedAdmissionLosses, []RetainedAdmissionLoss{want}) { t.Fatal("holder replacement or retired cap erased loss") } }) } } func TestNativeRetentionDoesNotReplaceSignedLossArchive(t *testing.T) { c := &Claim{Holder: &Holder{Admissions: []AdmissionRecord{{Session: 3, LossID: "loss_1", WritesLost: &WritesLost{Box: "box"}}}}} if err := retainNativeAdmissionLosses(c); !errors.Is(err, ErrLossEvidencePending) || len(c.RetainedAdmissionLosses) != 0 { t.Fatalf("signed archive bypass: %v", err) } }