| index 5f22d9dbf..c4b87cf39 100644 |
| --- a/internal/host/plan10_j2_adapter_test.go |
| +++ b/internal/host/plan10_j2_adapter_test.go |
| @@ -2,6 +2,7 @@ package host |
| |
| import ( |
| "context" |
| + "encoding/json" |
| "errors" |
| "fmt" |
| "os/exec" |
| @@ -353,6 +354,70 @@ func TestPlan10J2MissingWakeKeepsRecoveryAtProbing(t *testing.T) { |
| // With a wake entry the same production adapter drives the real recovery |
| // pass: each return records the inputs it renders at probing, before the |
| // wake, and the recovery goes on to opened. |
| +// The production adapter accepts the claim's legacy encoding of a source |
| +// generation (whole-branch fixes, item 4): "" is a source whose sessions |
| +// predate admission records, which recoverRun.probing passes through |
| +// from the claim's journal unchanged and the owners normalise as @root. |
| +// A declared return and a clean return of such a source reach the wake; |
| +// a request naming another generation than the claim's still does not. |
| +func TestPlan10J2AdapterAcceptsALegacySourceGeneration(t *testing.T) { |
| + ctx := context.Background() |
| + for name, declared := range map[string]bool{"a clean return": false, "a declared return": true} { |
| + t.Run(name, func(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + created := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) |
| + inc, err := hostclaim.Incarnation("bab", "app", p1Host, created) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rec := provision.Record{Host: p1Host, Account: "bab", App: "app", Region: "ie", |
| + Move: &provision.MoveState{ID: p1ID, Incarnation: inc, From: "ie", To: "uk", State: provision.MoveRollingBack, |
| + Rollback: &provision.MoveRollback{ID: "rb_legacy", SourcePhase: provision.RbSourceProbing, TargetPhase: provision.RbTargetReleased}}} |
| + journal := map[string]string{hostclaim.JournalSrcGen: "", hostclaim.JournalSourceStamp: "15", hostclaim.JournalCutSession: "15"} |
| + if declared { |
| + lossKey, err := hostclaim.LossRecordKey("l1") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + rec.Move.Rollback.LossRef = lossKey |
| + journal["loss.l1"] = `{"kind":"move-target-lost","target":"uk-box"}` |
| + } |
| + c := &hostclaim.Claim{V: 2, Host: p1Host, Gen: 9, |
| + Holder: &hostclaim.Holder{Family: "instance", Key: "apps/bab/app/instances/" + p1Host + ".json", Account: "bab", App: "app", Incarnation: inc, CreatedAt: created, Location: "instances/" + p1Host, Stamp: 15, Session: 20, WriterBox: "uk-box", |
| + Admissions: []hostclaim.AdmissionRecord{{Session: 20, Box: "uk-box", Generation: p1ID, Region: "uk", PredecessorRegion: "ie", Mode: "cold", Coverage: "sealed"}}}, |
| + Op: hostclaim.Op{Kind: hostclaim.KindRestoring, ID: p1ID, Phase: hostclaim.PhaseProbing, Exec: "ie-box", Participants: []string{"ie-box", "uk-box"}, |
| + Intent: map[string]string{"to": "uk", "source": "ie-box", "target": "uk-box", "rollback": "rb_legacy"}, Journal: journal}} |
| + if declared { |
| + c.LossRefs, c.Lost = []string{"l1"}, []string{"uk-box"} |
| + } |
| + key, _ := hostclaim.Key(p1Host) |
| + body, _ := json.Marshal(c) |
| + if _, err := s.Put(ctx, key, body, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + woke := 0 |
| + a := recoveryProbeAdapter{Box: "ie-box", Claims: true, |
| + Route: func(string) (*registry.Route, error) { return ®istry.Route{Host: p1Host, DBPath: "/data/x.db"}, nil }, |
| + Current: func(context.Context, release.Store, provision.Record) (provision.MoveInputs, error) { |
| + return provision.MoveInputs{Release: "v1"}, nil |
| + }, |
| + Wake: func(context.Context, registry.Route, activator.StartPolicy) error { woke++; return nil }} |
| + req := recoveryProbeRequest{RollbackID: "rb_legacy", Clean: true, Declared: declared, Attempts: recoveryProbeAttempts, SourceGeneration: "", |
| + Record: func(context.Context, provision.MoveInputs) error { return nil }} |
| + if !declared { |
| + req.Coverage = recoveryCoverageClean |
| + } |
| + if out, err := a.probe(ctx, s, rec, req); err != nil || out != recoveryProbeAnswered || woke != 1 { |
| + t.Fatalf("a legacy source's return: %v %v woke=%d", out, err, woke) |
| + } |
| + req.SourceGeneration = "other" |
| + if _, err := a.probe(ctx, s, rec, req); !errors.Is(err, hostbox.ErrChanged) || woke != 1 { |
| + t.Fatalf("another generation than the claim's: %v woke=%d", err, woke) |
| + } |
| + }) |
| + } |
| +} |
| + |
| func TestPlan10J2AdapterDrivesTheRecoveryPass(t *testing.T) { |
| for _, dirty := range []bool{false, true} { |
| t.Run(fmt.Sprintf("dirty=%v", dirty), func(t *testing.T) { |
| @@ -396,7 +461,7 @@ func TestPlan10J2AdapterDrivesTheRecoveryPass(t *testing.T) { |
| t.Fatalf("wakes %+v probes %d", wakes, len(h.probes)) |
| } |
| req := h.probes[0] |
| - if req.Clean == dirty || req.Attempts != recoveryProbeAttempts || (dirty && (req.Coverage != recoveryCoverageSealed || req.Generation == "")) || (!dirty && (req.Coverage != recoveryCoverageClean || req.SourceGeneration == "")) { |
| + if req.Clean == dirty || req.Attempts != recoveryProbeAttempts || (dirty && (req.Coverage != recoveryCoverageSealed || req.Generation == "")) || (!dirty && (req.Coverage != recoveryCoverageClean || req.SourceGeneration != h.claim(t).Op.Journal[hostclaim.JournalSrcGen])) { |
| t.Fatalf("request %+v", req) |
| } |
| }) |