| index 46a043141..8a1a41055 100644 |
| --- a/internal/hostclaim/relocation_test.go |
| +++ b/internal/hostclaim/relocation_test.go |
| @@ -1096,3 +1096,42 @@ func TestRelocationParksAPDroppedFromProbeFailed(t *testing.T) { |
| t.Fatalf("P1 was not parked: %+v", o.Claim.Obligations) |
| } |
| } |
| + |
| +// Fix round 1: a move converted to relocating journals the source's fence |
| +// without its observation time. The claim refuses a pin until FenceSource |
| +// has consumed that fence, and then holds it FailoverPinDelay past the |
| +// fence's observed_at (H3), as it does for a failover. |
| +func TestRelocationConvertedMovePinsOnlyAfterFenceSourceAndTheHold(t *testing.T) { |
| + ctx := context.Background() |
| + s, o, ref, a := c7Fixture(t) |
| + o, err := ConvertMovingToRelocating(ctx, s, o, "move-1", ref, a, &c7Guard{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + observed, err := time.Parse(time.RFC3339Nano, a.want.ObservedAt) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + pin := func(o Observed, at time.Time) RecoveryPin { |
| + return RecoveryPin{Key: RecoveryPinKey(o.Claim.Host, "move-1", o.Claim.Gen), SHA256: strings.Repeat("a", 64), Generation: "gen-current", MaxTXID: 40, |
| + FilesManifest: "instances/x/files/gen-current/manifest.json", FilesSeq: 7, PinnedAt: at} |
| + } |
| + for _, late := range []time.Duration{time.Second, time.Hour} { |
| + if _, err := Update(ctx, s, o, JournalPin("move-1", pin(o, observed.Add(late)))); !errors.Is(err, ErrRelocation) { |
| + t.Fatalf("pinned %s after a fence FenceSource never consumed: %v", late, err) |
| + } |
| + } |
| + if o, err = FenceSource(ctx, s, o, "move-1", ref, nil, a); err != nil { |
| + t.Fatal(err) |
| + } |
| + if o.Claim.Op.Journal["fence.adi-knitting.observed_at"] != a.want.ObservedAt || !slices.Contains(o.Claim.Lost, "adi-knitting") { |
| + t.Fatalf("FenceSource on the converted move: %+v lost %v", o.Claim.Op.Journal, o.Claim.Lost) |
| + } |
| + if _, err := Update(ctx, s, o, JournalPin("move-1", pin(o, observed.Add(FailoverPinDelay-time.Second)))); !errors.Is(err, ErrRelocation) { |
| + t.Fatalf("pinned inside the 30-second window: %v", err) |
| + } |
| + o = step(t, s, o, JournalPin("move-1", pin(o, observed.Add(FailoverPinDelay)))) |
| + if !readJournal(o.Claim.Op, "pin", new(RecoveryPin)) { |
| + t.Fatalf("pin not journaled: %+v", o.Claim.Op.Journal) |
| + } |
| +} |