| index 766d87775..80bfc69ac 100644 |
| --- a/internal/host/movesrecoveryowners_test.go |
| +++ b/internal/host/movesrecoveryowners_test.go |
| @@ -33,6 +33,9 @@ const ( |
| rcDirty = "dirty" |
| rcClean = "clean" |
| rcRevert = "revert" |
| + // rcLegacy is a clean return of a legacy source: src_gen is the origin |
| + // (""), its files at the location's root and no generation.json. |
| + rcLegacy = "legacy" |
| // rcS is the source installation's stamp S the flip (or the revert) |
| // journalled. |
| rcS = 15 |
| @@ -82,9 +85,12 @@ func newRCFixture(t *testing.T, kind string) *rcFixture { |
| at := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) |
| |
| // The source's own generation in its region, and the marker naming it. |
| - srcKey := "instances/" + poHost + "/g-src/0009/" + ltx.FormatFilename(1, 30) |
| - f.bucket.Put("data", srcKey, f.ltxHeader(1, 30)) |
| - f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: "ie-box", InstalledAt: at})) |
| + if kind == rcLegacy { |
| + f.bucket.Put("data", "instances/"+poHost+"/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30)) |
| + } else { |
| + f.bucket.Put("data", "instances/"+poHost+"/g-src/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30)) |
| + f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: "ie-box", InstalledAt: at})) |
| + } |
| |
| c := f.recoveryClaim() |
| f.rec.Region = "ie" |
| @@ -137,7 +143,11 @@ func newRCFixture(t *testing.T, kind string) *rcFixture { |
| if err := os.Remove(filepath.Join(f.files, archive.MarkerName)); err != nil { |
| t.Fatal(err) |
| } |
| - if err := hostbox.WriteGenerationMarker(f.route.DBPath+".gen", "src"); err != nil { |
| + if kind == rcLegacy { |
| + if err := os.Remove(f.route.DBPath + ".gen"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err := hostbox.WriteGenerationMarker(f.route.DBPath+".gen", "src"); err != nil { |
| t.Fatal(err) |
| } |
| } |
| @@ -165,6 +175,12 @@ func (f *rcFixture) recoveryClaim() *hostclaim.Claim { |
| switch f.kind { |
| case rcClean: |
| h.Generation = "src" |
| + case rcLegacy: |
| + // The source never named a generation: its sessions predate |
| + // admission records, and the landing's admission came from the origin. |
| + h.Generation, h.Admissions = "", h.Admissions[1:] |
| + h.Admissions[0].Predecessor = "" |
| + journal[hostclaim.JournalSrcGen] = "" |
| case rcRevert: |
| h.Generation, h.Session, h.WriterBox, h.Admissions = "src", 15, "ie-box", h.Admissions[:1] |
| op.Kind = hostclaim.KindReverting |
| @@ -411,6 +427,35 @@ func TestCleanReturnProbeIsCoveredCleanReturn(t *testing.T) { |
| t.Fatalf("the restart after the crash: %v", err) |
| } |
| }) |
| + // Fix round 2: a legacy source's src_gen is the origin, "" in the claim |
| + // and hostclaim.RootGeneration in a selection; the source baseline and |
| + // its pin compare the two as one. |
| + t.Run("a legacy source's clean return", func(t *testing.T) { |
| + f := newRCFixture(t, rcLegacy) |
| + ctx := t.Context() |
| + o := f.read() |
| + in := f.selectAdmission(o) |
| + if a := in.Admission; a.NewGeneration != "fresh1" || a.Coverage != "clean-return" || a.ObservedGeneration != "" || a.RemoteMaxTXID != 30 || |
| + in.Baseline.Generation != hostclaim.RootGeneration { |
| + t.Fatalf("admission %+v baseline %+v", a, in.Baseline) |
| + } |
| + admitted := f.admitThroughWriter(o, in) |
| + again, err := f.owners.Select(ctx, f.route, admitted, f.policy()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if again.Baseline.Generation != hostclaim.RootGeneration || again.Admission.RemoteMaxTXID != 30 || again.Admission.Coverage != "" { |
| + t.Fatalf("restart %+v baseline %+v", again.Admission, again.Baseline) |
| + } |
| + if err := f.owners.AdmissionEvidence(ctx, admitted, again.Admission, again.Baseline); err != nil { |
| + t.Fatal(err) |
| + } |
| + // The origin's pin was journalled at the first admission. |
| + f.bucket.Put("data", "instances/"+poHost+"/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30)) |
| + if _, err := f.owners.Select(ctx, f.route, admitted, f.policy()); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatalf("another version of the origin: %v", err) |
| + } |
| + }) |
| // A declared or fenced return (Q6) asks no coverage: the claim journals |
| // the fence and the record carries it as its loss. |
| t.Run("a fenced return asks no coverage", func(t *testing.T) { |