package hostclaim import ( "context" "encoding/json" "errors" "fmt" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/release" ) func TestAdmitWriterReturnRegion(t *testing.T) { for _, mode := range []string{"warm", "cold"} { for _, region := range []string{"ie", ""} { t.Run(mode+"/region="+region, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"})) target := o.Claim.Holder.Admissions[0] o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"} return nil }) in := Admission{ ObservedOpID: "move", ObservedPhase: "return", ObservedSession: target.Session, ObservedGeneration: "GT", Box: "boxS", Mode: mode, Region: region, PredecessorRegion: "ignored", PredecessorCertified: true, Coverage: "clean-return", } if mode == "cold" { in.NewGeneration = "GS2" } next, err := Update(ctx, s, o, AdmitWriter(in)) if mode == "warm" || region == "" { if !errors.Is(err, ErrWrongState) { t.Fatalf("return admission = %v, want ErrWrongState", err) } assertAdmissionStored(t, ctx, s, o) return } if err != nil { t.Fatal(err) } last := next.Claim.Holder.Admissions[1] if last.Region != "ie" || last.PredecessorRegion != "uk" || last.Predecessor != "GT" || last.Generation != "GS2" || last.Mode != "cold" || last.Covered { t.Fatalf("cold return = %+v", last) } assertAdmissionStored(t, ctx, s, next) }) } } } func TestAdmitWriterPendingParkCoverage(t *testing.T) { for _, name := range []string{"whole predecessor region", "scoped holder location", "no park", "other region", "generation only", "local only", "scoped other location"} { t.Run(name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"})) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"} ob := Obligation{ID: "park", Op: "release", Kind: "park-replica", Region: "uk", Owner: "region:uk", Retirement: "ret"} switch name { case "no park": return nil case "other region": ob.Region, ob.Owner = "ie", "region:ie" case "generation only": ob.Kind, ob.Generation = "park-generation", "GT" case "local only": ob.Kind, ob.Region, ob.Owner, ob.Box = "park-local", "", "box:boxT", "boxT" case "scoped holder location", "scoped other location": loc := c.Holder.Location if name == "scoped other location" { loc = "moves/" + c.Host + "/oldmove" } ob.Park = &ParkScope{Location: loc, Incarnation: c.Holder.Incarnation, EvidenceAt: time.Unix(1, 0).UTC(), RequestLimit: time.Second} } c.Obligations = []Obligation{ob} return nil }) before := o o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ ObservedOpID: "move", ObservedPhase: "return", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GT", Box: "boxS", Mode: "cold", Region: "ie", NewGeneration: "GS2", })) last := o.Claim.Holder.Admissions[1] if last.Covered != (name == "whole predecessor region" || name == "scoped holder location") || last.PredecessorRegion != "uk" || last.WritesLost == nil { t.Fatalf("return admission = %+v", last) } if !reflect.DeepEqual(before.Claim.Obligations, o.Claim.Obligations) { t.Fatal("admission changed park obligations") } }) } } func TestPublishGenerationServingCyclesBoundAdmissions(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) newID := counter() for cycle := 1; cycle <= 20; cycle++ { generation := fmt.Sprintf("G%d", cycle) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", PredecessorRegion: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, // Exercise trimming without unarchived loss evidence. Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation, PredecessorCertified: true, })) o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, generation, "ie", newID)) for _, record := range o.Claim.Holder.Admissions { if !record.Covered { t.Fatalf("cycle %d left superseded predecessor uncovered: %+v", cycle, record) } } if len(o.Claim.Obligations) != 1 { t.Fatalf("cycle %d chores = %+v", cycle, o.Claim.Obligations) } predecessor := RootGeneration if cycle > 1 { predecessor = fmt.Sprintf("G%d", cycle-1) } ob := o.Claim.Obligations[0] if ob.Generation != predecessor { t.Fatalf("retiring %q, want %q", ob.Generation, predecessor) } var err error o, err = Update(ctx, s, o, CompleteObligation(ob.Op, ob.ID)) if err != nil { t.Fatal(err) } o, err = Read(ctx, s, testHost) if err != nil { t.Fatal(err) } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: generation, Box: o.Claim.Op.Exec, Mode: "warm", PredecessorCertified: true, })) o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, generation, "ie", newID)) records := o.Claim.Holder.Admissions if !records[len(records)-1].Covered || len(o.Claim.Obligations) != 0 { t.Fatalf("cycle %d left warm entry uncovered or retired live predecessor", cycle) } if len(records) > 8 { t.Fatalf("cycle %d admissions grew to %d", cycle, len(records)) } } } func TestPublishGenerationReturnedSourceRetiredLater(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Holder.Generation = "GS" c.Holder.Admissions = []AdmissionRecord{ {Region: "uk", Generation: "GT", Predecessor: "GS", PredecessorRegion: "ie"}, {Region: "ie", Generation: "GS", Predecessor: "GT", PredecessorRegion: "uk"}, } return nil }) o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "GS", "ie", counter())) if o.Claim.Holder.Admissions[0].Covered || o.Claim.Holder.Admissions[1].Covered || len(o.Claim.Obligations) != 0 { t.Fatal("source publication covered a live or foreign predecessor") } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GS", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS2", })) o = updateGenerationTest(t, ctx, s, o, PublishGeneration("", "", o.Claim.Holder.Session, "GS2", "ie", counter())) if !o.Claim.Holder.Admissions[0].Covered || o.Claim.Holder.Admissions[1].Covered || !o.Claim.Holder.Admissions[2].Covered || len(o.Claim.Obligations) != 1 || o.Claim.Obligations[0].Generation != "GS" { t.Fatalf("source retirement after return = %+v; admissions = %+v", o.Claim.Obligations, o.Claim.Holder.Admissions) } } func TestCoverParkedPredecessors(t *testing.T) { for _, kind := range []string{KindServing, KindParking} { t.Run(kind, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { if kind == KindParking { c.Op = Op{Kind: kind, ID: "sweep", Phase: "park", Intent: map[string]string{"park_sweep": "v1"}} } c.Holder.Admissions = []AdmissionRecord{ {Region: "ie", PredecessorRegion: "ie", Predecessor: "G1"}, {Region: "uk", PredecessorRegion: "ie", Predecessor: ""}, {Region: "ie", PredecessorRegion: "ie", Predecessor: RootGeneration}, {Region: "ie", PredecessorRegion: "uk", Predecessor: "G1"}, {Region: "ie", PredecessorRegion: "ie", Predecessor: "G2"}, {Region: "ie", Predecessor: "G1"}, {Region: "ie", PredecessorRegion: "ie", Predecessor: "G3", Covered: true}, } c.Obligations = []Obligation{{ID: "existing", Op: "old", Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "ret", Owner: "region:ie"}} return nil }) before := o for _, generations := range [][]string{nil, {}, {""}} { o = updateGenerationTest(t, ctx, s, o, CoverParkedPredecessors(o.Claim.Op.ID, o.Claim.Op.Phase, "ie", generations)) if !reflect.DeepEqual(o.Claim.Holder, before.Claim.Holder) { t.Fatalf("empty selection changed records: %+v", o.Claim.Holder.Admissions) } } want := *o.Claim h := *want.Holder want.Holder = &h h.Admissions = append([]AdmissionRecord(nil), h.Admissions...) for i := 0; i < 3; i++ { h.Admissions[i].Covered = true } for repeat := 0; repeat < 2; repeat++ { o = updateGenerationTest(t, ctx, s, o, CoverParkedPredecessors(o.Claim.Op.ID, o.Claim.Op.Phase, "ie", []string{"G1", RootGeneration, "G1", "absent"})) want.Gen++ if !reflect.DeepEqual(o.Claim, &want) { t.Fatalf("sweep changed more than selected coverage: %+v", o.Claim) } } }) } } func TestCoverParkedPredecessorsRefusals(t *testing.T) { for _, name := range []string{"stale op", "stale phase", "nil holder", "empty region"} { t.Run(name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindParking, ID: "sweep", Phase: "park"} c.Holder.Admissions = []AdmissionRecord{{Region: "ie", PredecessorRegion: "ie", Predecessor: "G1"}} if name == "nil holder" { c.Holder, c.Op = nil, Op{} } return nil }) op, phase, region := o.Claim.Op.ID, o.Claim.Op.Phase, "ie" switch name { case "stale op": op = "old" case "stale phase": phase = "old" case "empty region": region = "" } if _, err := Update(ctx, s, o, CoverParkedPredecessors(op, phase, region, []string{"G1"})); !errors.Is(err, ErrWrongState) { t.Fatalf("sweep = %v", err) } assertAdmissionStored(t, ctx, s, o) }) } } func TestCoverParkedPredecessorsComposesWithRetirement(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) cover := CoverParkedPredecessors("", "", "ie", []string{"G1"}) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Holder.Admissions = []AdmissionRecord{{Region: "uk", PredecessorRegion: "ie", Predecessor: "G1"}} c.Obligations = append(c.Obligations, Obligation{ID: "park", Op: "sweep", Kind: "park-generation", Region: "ie", Generation: "G1", Retirement: "ret", Owner: "region:ie"}) return cover(c) }) if !o.Claim.Holder.Admissions[0].Covered { t.Fatal("queue CAS did not cover predecessor") } o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Holder.Admissions = append(c.Holder.Admissions, AdmissionRecord{Region: "uk", PredecessorRegion: "ie", Predecessor: "G1"}) return nil }) next, err := Update(ctx, s, o, func(c *Claim) error { if err := cover(c); err != nil { return err } return CompleteObligation("sweep", "park")(c) }) if err != nil { t.Fatal(err) } stored, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } if stored.ETag != next.ETag || len(stored.Claim.Obligations) != 0 || !stored.Claim.Holder.Admissions[1].Covered { t.Fatal("acknowledgement CAS did not cover later predecessor") } } func TestAdmitWriterAdditionalWritesLost(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS", PredecessorCertified: true})) cut := o.Claim.Holder.Session o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Op = Op{Kind: KindLanding, ID: "move", Phase: "probe", Exec: "boxT"} return nil }) var target []AdmissionRecord for _, mode := range []string{"cold", "warm", "warm"} { in := Admission{Region: "uk", ObservedOpID: "move", ObservedPhase: "probe", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "boxT", Mode: mode, PredecessorCertified: true} if mode == "cold" { in.NewGeneration, in.Coverage = "GT", "sealed" } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(in)) target = append(target, o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1]) } o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Holder.Generation = "GS" c.Op = Op{Kind: KindReverting, ID: "move", Phase: "return", Exec: "boxS"} return nil }) additional := []WritesLost{} for i, record := range target[:2] { if record.Session <= cut { t.Fatal("target session does not follow the cut") } additional = append(additional, WritesLost{Box: record.Box, Session: record.Session, Generation: record.Generation, RemoteMaxTXID: uint64(10 + i)}) } wantAdditional := append([]WritesLost(nil), additional...) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", ObservedOpID: "move", ObservedPhase: "return", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GS", Box: "boxS", Mode: "cold", NewGeneration: "GS2", RemoteMaxTXID: 42, AdditionalWritesLost: additional, })) last := o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1] wantPrimary := &WritesLost{Box: "boxT", Session: target[2].Session, Generation: "GT", RemoteMaxTXID: 42} if !reflect.DeepEqual(last.WritesLost, wantPrimary) || !reflect.DeepEqual(last.AdditionalWritesLost, wantAdditional) { t.Fatalf("loss declarations = %+v, %+v", last.WritesLost, last.AdditionalWritesLost) } additional[0].Box = "changed" if !reflect.DeepEqual(last.AdditionalWritesLost, wantAdditional) { t.Fatal("record aliases the caller's loss declarations") } raw, err := json.Marshal(last) if err != nil { t.Fatal(err) } var fields map[string]json.RawMessage if err := json.Unmarshal(raw, &fields); err != nil { t.Fatal(err) } var decoded []WritesLost if err := json.Unmarshal(fields["additional_writes_lost"], &decoded); err != nil { t.Fatal(err) } if !reflect.DeepEqual(decoded, wantAdditional) { t.Fatalf("additional loss JSON = %s", raw) } assertAdmissionStored(t, ctx, s, o) } func TestAdoptLostTargetReturnDeclaresLegacySource(t *testing.T) { ctx := t.Context() s := release.NewDirStore(t.TempDir()) o, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } o = updateGenerationTest(t, ctx, s, o, Adopt(newHolder(), "boxS", nil, nil, "adopt", counter())) cut := WritesLost{Box: o.Claim.Holder.WriterBox, Session: o.Claim.Holder.Session, Generation: o.Claim.Holder.Generation, RemoteMaxTXID: 17} if cut.Box != "boxS" || cut.Session != 0 || cut.Generation != "" { t.Fatalf("legacy cut = %+v", cut) } o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { if err := StartGuard(c, []string{"ie", "uk"}, []string{"boxS", "boxT"}, ""); err != nil { return err } c.Op = Op{Kind: KindMoving, ID: "move", Phase: "cut", Exec: "boxS", Participants: []string{"boxS", "boxT"}, Journal: map[string]string{"src_covered": "false"}} return nil }) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Op.Kind, c.Op.Phase, c.Op.Exec = KindLanding, "probe", "boxT" return nil }) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ ObservedOpID: "move", ObservedPhase: "probe", ObservedSession: cut.Session, ObservedGeneration: cut.Generation, Region: "uk", PredecessorRegion: "ie", Box: "boxT", Mode: "cold", NewGeneration: "GT", RemoteMaxTXID: cut.RemoteMaxTXID, })) target := o.Claim.Holder.Admissions[0] if !reflect.DeepEqual(target.WritesLost, &cut) { t.Fatalf("primary legacy loss = %+v, want %+v", target.WritesLost, cut) } o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { c.Holder.Generation = cut.Generation c.Lost = append(c.Lost, "boxT") c.Op.Kind, c.Op.Phase, c.Op.Exec = KindReverting, "return", "boxS" return nil }) in := Admission{ ObservedOpID: "move", ObservedPhase: "return", ObservedSession: target.Session, ObservedGeneration: cut.Generation, Region: "ie", Box: "boxS", Mode: "cold", NewGeneration: "GS2", RemoteMaxTXID: 42, AdditionalWritesLost: []WritesLost{cut}, } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(in)) last := o.Claim.Holder.Admissions[len(o.Claim.Holder.Admissions)-1] wantPrimary := &WritesLost{Box: target.Box, Session: target.Session, Generation: target.Generation, RemoteMaxTXID: 42} if !reflect.DeepEqual(last.WritesLost, wantPrimary) || !reflect.DeepEqual(last.AdditionalWritesLost, []WritesLost{cut}) { t.Fatalf("return losses = %+v, %+v", last.WritesLost, last.AdditionalWritesLost) } if _, err := Update(ctx, s, o, AdmitWriter(in)); !errors.Is(err, ErrWrongState) { t.Fatalf("repeated admission = %v", err) } assertAdmissionStored(t, ctx, s, o) } func TestAdmitWriterAdditionalWritesLostValidation(t *testing.T) { for _, name := range []string{"empty box", "zero session", "empty generation", "duplicate session", "duplicate predecessor", "same box different session", "same session different box", "root generation", "legacy identity", "legacy empty box", "duplicate legacy", "warm", "sealed"} { t.Run(name, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "uk", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GT"})) loss := WritesLost{Box: "boxT", Session: 42, Generation: "GT", RemoteMaxTXID: 0} in := Admission{Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "GT", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "GS"} switch name { case "empty box": loss.Box = "" case "zero session": loss.Session = 0 case "empty generation": loss.Generation = "" case "duplicate predecessor": loss.Box, loss.Session = o.Claim.Holder.WriterBox, o.Claim.Holder.Session case "root generation": loss.Generation = RootGeneration case "legacy identity", "legacy empty box", "duplicate legacy": loss.Session, loss.Generation = 0, "" if name == "legacy empty box" { loss.Box = "" } case "warm": in.Mode, in.NewGeneration, in.PredecessorCertified = "warm", "", true in.Region = "uk" case "sealed": in.Coverage = "sealed" } in.AdditionalWritesLost = []WritesLost{loss} switch name { case "duplicate legacy": in.AdditionalWritesLost = append(in.AdditionalWritesLost, loss) case "duplicate session", "same box different session", "same session different box": second := loss second.Generation, second.RemoteMaxTXID = "other", 99 if name == "same box different session" { second.Session++ } if name == "same session different box" { second.Box = "boxU" } in.AdditionalWritesLost = append(in.AdditionalWritesLost, second) } next, err := Update(ctx, s, o, AdmitWriter(in)) switch name { case "empty box", "zero session", "empty generation", "duplicate session", "duplicate predecessor", "legacy empty box", "duplicate legacy": if !errors.Is(err, ErrWrongState) { t.Fatalf("invalid loss admission = %v", err) } assertAdmissionStored(t, ctx, s, o) default: if err != nil { t.Fatal(err) } last := next.Claim.Holder.Admissions[len(next.Claim.Holder.Admissions)-1] if !reflect.DeepEqual(last.AdditionalWritesLost, in.AdditionalWritesLost) { t.Fatalf("loss declarations = %+v", last.AdditionalWritesLost) } assertAdmissionStored(t, ctx, s, next) } }) } } func TestAdmitWriterWarmBornCovered(t *testing.T) { for _, generation := range []string{"", "G1"} { t.Run("generation="+generation, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) if generation != "" { o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", PredecessorRegion: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: generation, })) } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", PredecessorRegion: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: generation, Box: o.Claim.Op.Exec, Mode: "warm", PredecessorCertified: true, })) records := o.Claim.Holder.Admissions last := records[len(records)-1] if !last.Covered || last.Mode != "warm" || last.Generation != generation || last.Predecessor != generation || last.WritesLost != nil || len(o.Claim.Obligations) != 0 { t.Fatalf("warm birth = %+v; chores = %+v", last, o.Claim.Obligations) } if len(records) > 1 && records[0].Covered { t.Fatal("warm birth covered an earlier cold entry") } assertAdmissionStored(t, ctx, s, o) }) } } func TestCompleteObligationDoesNotCoverAdmissions(t *testing.T) { for _, kind := range []string{"park-replica", "park-generation"} { t.Run(kind, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{ Region: "ie", PredecessorRegion: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1", })) o = updateGenerationTest(t, ctx, s, o, func(c *Claim) error { ob := Obligation{ID: "park", Op: "cleanup", Kind: kind, Region: "ie", Owner: "region:ie", Retirement: "ret"} if kind == "park-generation" { ob.Generation = RootGeneration } c.Obligations = append(c.Obligations, ob) return nil }) before := o.Claim.Holder.Admissions if before[0].Covered { t.Fatal("queueing a park alone covered its predecessor") } next, err := Update(ctx, s, o, CompleteObligation("cleanup", "park")) if err != nil { t.Fatal(err) } stored, err := Read(ctx, s, testHost) if err != nil { t.Fatal(err) } if stored.ETag != next.ETag || len(stored.Claim.Obligations) != 0 || !reflect.DeepEqual(stored.Claim.Holder.Admissions, before) { t.Fatalf("acknowledgement changed admissions: %+v", stored.Claim) } }) } } // A created incarnation's first admission declares nothing: its predecessor // has no writer, so no write was ever acknowledged (moves 7.17 note 2). func TestAdmitWriterFirstAdmissionAfterCreateDeclaresNothing(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) o := servingClaim(t, ctx, s) if o.Claim.Holder.WriterBox != "" || len(o.Claim.Holder.Admissions) != 0 { t.Fatalf("fixture is not a fresh create: %+v", o.Claim.Holder) } o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G1"})) first := o.Claim.Holder.Admissions[0] if first.WritesLost != nil || len(first.AdditionalWritesLost) != 0 { t.Fatalf("first admission declared a loss: %+v", first) } // The next uncertified cold admission does declare its predecessor. o = updateGenerationTest(t, ctx, s, o, AdmitWriter(Admission{Region: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: "G1", Box: o.Claim.Op.Exec, Mode: "cold", NewGeneration: "G2"})) second := o.Claim.Holder.Admissions[1] if second.WritesLost == nil || second.WritesLost.Box == "" || second.WritesLost.Generation != "G1" { t.Fatalf("second admission = %+v", second) } } // writes_lost keeps its wire shape when a real predecessor is declared. func TestWritesLostJSONShape(t *testing.T) { b, err := json.Marshal(AdmissionRecord{Region: "ie", Session: 4, Box: "boxB", Generation: "G2", Mode: "cold", WritesLost: &WritesLost{Box: "boxA", Generation: "G1", Session: 3, RemoteMaxTXID: 42}}) if err != nil { t.Fatal(err) } var got map[string]any if err := json.Unmarshal(b, &got); err != nil { t.Fatal(err) } want := map[string]any{"box": "boxA", "generation": "G1", "session": float64(3), "remote_max_txid": float64(42)} if !reflect.DeepEqual(got["writes_lost"], want) { t.Fatalf("writes_lost = %v, want %v", got["writes_lost"], want) } }