//go:build linux package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "io/fs" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "sort" "strings" "testing" "time" "crypto/sha256" "encoding/hex" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // The offline move's landing, open on the target and the target's revert // (Task 5 of docs/superpowers/plans/2026-10-20-offline-operator-move.md), // tested through the functions the agent calls (newMovesWorker, armOffline, // reconcileSerializedWith, the edges-file writer and movesPass), over one // DirStore for the deployment bucket, leasetest's S3 server for the data // buckets data-ie and data-uk, real registries, a real litestream replica, // the real archive, the real held render and the exec backend's real launch // composition. // // The seal is Task 4's: the source box ie-a runs the sealer step // (offlineSealerStep, through offlineRound) over T4's seal world, so the // staged replica, the archive, its manifest, the timers snapshot, the env // report and seal.json are exactly what the sealer writes. const ( landKeyValue = "the-instance-key" // the key sealWorld.instance writes landStore = "shop-store-ie" landSecret = "store-secret-value" ) // landS3 reads the data buckets' model directly. type landS3 struct{ f *leasetest.VersionedBucket } // current is the current objects under prefix in bucket. func (s *landS3) current(t *testing.T, bucket, prefix string) map[string][]byte { t.Helper() vers, err := s.f.ListVersions(context.Background(), bucket, prefix) if err != nil { t.Fatal(err) } out := map[string][]byte{} for _, v := range vers { if v.IsLatest && !v.DeleteMarker { body, _ := s.f.Current(bucket, v.Key) out[v.Key] = body } } return out } // landOpts shapes the source: configKey sets the instance key in the app's // configuration (and none in .env), noFiles gives the host no files // directory, nested makes it a nested row with the launch descriptors a real // nested host keeps. type landOpts struct { configKey string noFiles bool nested bool } // landWorld is offHost at home (ie-a, data-ie) moving to uk (uk-box, // data-uk), with a real database replica, a files directory, an env file // carrying the instance key, an app config bundle carrying the store // binding, timers, a NextDue, a replication rung and a promoted release, // sealed by the real sealer. type landWorld struct { t *testing.T sw *sealWorld wd *offWorld s3 *landS3 ie *offBox uk *offBox ukEB *activator.ExecBackend relPriv ed25519.PrivateKey seal moves.OfflineReport sealRaw []byte src provision.MoveSource opts landOpts // The source's files directory, env file and config file. srcFiles, srcEnv, srcConfig string } func newLandWorld(t *testing.T) *landWorld { return newLandWorldWith(t, landOpts{}) } func newLandWorldWith(t *testing.T, opts landOpts) *landWorld { t.Helper() ctx := context.Background() sw := newSealWorld(t) lw := &landWorld{t: t, sw: sw, wd: sw.offWorld, s3: &landS3{f: sw.fake}, ie: sw.ie, uk: sw.uk, opts: opts} seed, pub, err := release.GenerateKey() if err != nil { t.Fatal(err) } if lw.relPriv, err = release.ParseSigningKey(seed, "test"); err != nil { t.Fatal(err) } shipVersion(t, sw.s, sw.acct, "app", sealVersion) shipVersion(t, sw.s, sw.acct, "app", "v2") lw.promote(sealVersion) lw.bundle(landStore) lw.uk.env["CARLOS_HIBERNATE_REGION"] = sealUKRegion lw.uk.env["CARLOS_RELEASE_PUBKEY"] = pub lw.ukEB = &activator.ExecBackend{DataDir: lw.uk.dd, OptDir: lw.uk.opt, Region: "uk"} lw.arm(lw.uk) // The source at rest, as a box that has served it. ie := lw.ie sw.instance(ie) lw.srcFiles, lw.srcEnv, lw.srcConfig = filepath.Join(ie.dd, offHost), filepath.Join(ie.dd, offHost+".env"), filepath.Join(ie.dd, offHost+".config.env") if opts.nested { raw := sw.nest(ie) lw.srcFiles, lw.srcEnv, lw.srcConfig = filepath.Join(raw, offHost), filepath.Join(raw, offHost+".env"), filepath.Join(raw, offHost+".config.env") for _, name := range []string{"launch.run.json", "launch.seed.json", "launch.gen.7.json"} { writeFile(t, filepath.Join(raw, name), "{}") } // The source's env pin is composed from the instance directory's // own env and config files, as the agent's exec backend reads them // through the instance mount. The fixture's mount is a symlink, // which the nested reader refuses (and the seal world's backend, // with no socket directory, classifies the row foreign and reads // the flat paths), so here the raw directory stands in for the // mount: the same two files, composed by the same code. ie.w.cfg.LaunchLayers = func(rt registry.Route) (map[string]string, map[string]bool, []string, error) { rt.DBPath, rt.Socket = provision.DefaultDBPath(raw, rt.Host), filepath.Join(provision.SocketDir, rt.Host+".sock") return (&activator.ExecBackend{DataDir: raw}).LaunchLayers(rt) } } if opts.configKey != "" { writeFile(t, lw.srcEnv, "CARLOS_ADMIN_TOKEN=token\n") } if opts.noFiles { if err := os.RemoveAll(lw.srcFiles); err != nil { t.Fatal(err) } } // start and the source's hold; its config rendered held from the bundle // (as SyncExec renders it at rest); seal; the sealer's pass. sw.startAndSeal("ie-a", ie) hr, err := ie.w.cfg.RenderHeld(ctx, sw.s, *ie.row(offHost), offID, nil, func(string) error { return nil }) if err != nil { t.Fatal(err) } if hr.Path != lw.srcConfig { // The nested fixture's sockets are not production's, so the render // took the flat path; production renders into the instance // directory, which is where the row's launch reads it. if err := os.Rename(hr.Path, lw.srcConfig); err != nil { t.Fatal(err) } } sw.mustPass(ie) rec := sw.record() if rec.Move.State != provision.OfflineSealed { t.Fatalf("not sealed: %+v (%+v)", rec.Move, sw.report(moves.OfflineRoleSealer, "ie-a").Items) } var ok bool if lw.seal, lw.sealRaw, ok = sw.sealRecord(); !ok { t.Fatal("no seal.json") } lw.src = *rec.Move.Source return lw } // arm wires the target box as armMovesPass would, over its own data bucket. func (lw *landWorld) arm(b *offBox) { bucket, region := b.getenv("CARLOS_HIBERNATE_BUCKET"), b.getenv("CARLOS_HIBERNATE_REGION") b.svc = activator.New(activator.Config{Bucket: bucket, Idle: time.Hour}, &fakeWakeBackend{}, b.reg) cfg := moveCfg{Region: b.region, Box: b.name, DataDir: b.dd, OptDir: b.opt, Quiesced: b.svc.Quiesced, Route: b.reg.Get, Now: b.wd.clock, OwnBucket: bucket, OwnBucketRegion: region, OwnPrefix: sealPrefix, Mirror: func(ctx context.Context, dst string) (lease.MirrorStore, error) { return lease.NewS3Mirror(ctx, region, dst) }} armOffline(&cfg, b.dd, b.reg, b.svc, nil, hostwrite.Config{}, b.getenv) cfg.Offline = newOfflineView(cfg, nil) cfg.ReleaseHold = releaseHeld(b.reg, nil) cfg.Procs = func(host string) (int, error) { return cgroup.HostProcs(b.cg, host, "") } cfg.LaunchLayers = lw.ukEB.LaunchLayers b.w = newMovesWorker(cfg) } // restart is an agent restart on the target: a fresh activator and worker // over the same registry, data directory and buckets. func (lw *landWorld) restart() { lw.arm(lw.uk) } func (lw *landWorld) promote(v string) { lw.t.Helper() if err := release.Promote(context.Background(), lw.wd.s, lw.relPriv, lw.wd.acct, "app", "stable", v); err != nil { lw.t.Fatal(err) } } // bundle writes the app's config: the store binding, its secret and a // setting, and the instance key when the world's configuration sets it. func (lw *landWorld) bundle(store string) { lw.t.Helper() env, err := appconfig.SealBound(lw.sw.priv.PublicKey(), appconfig.Binding{Account: lw.wd.acct, App: "app"}, []byte(landSecret)) if err != nil { lw.t.Fatal(err) } m := appconfig.Manifest{Schema: 1, App: "app", Vars: map[string]appconfig.Var{ "STORE_BUCKET": {Kind: "env", Value: store}, "STORE_ENDPOINT": {Kind: "env", Value: "https://store.example.com"}, "STORE_SECRET_KEY": {Kind: "secret", Envelope: env}, "FEATURE": {Kind: "env", Value: "on"}, }} if lw.opts.configKey != "" { m.Vars[instancekey.Var] = appconfig.Var{Kind: "env", Value: lw.opts.configKey} } if err := moves.PutReplace(context.Background(), lw.wd.s, appconfig.Key(lw.wd.acct, "app"), mustJSON(lw.t, m)); err != nil { lw.t.Fatal(err) } } // flipAndLand flips the record and runs the target box's ticks. func (lw *landWorld) flipAndLand(ticks int) { lw.wd.flip() for range ticks { lw.uk.tick() } } func (lw *landWorld) report() moves.OfflineReport { lw.t.Helper() return lw.wd.report(moves.OfflineRoleTarget, "uk-box") } func (lw *landWorld) state() string { lw.t.Helper() rec := lw.wd.record() if rec.Move == nil { return "" } return rec.Move.State } // requireLanded is the happy landing's state: offline-landed, every landing // item OK in a landed report, the row held landing by the move, the edges // file not listing the host, and nothing ever taking the target's lease. func (lw *landWorld) requireLanded() { t := lw.t t.Helper() if st := lw.state(); st != provision.OfflineLanded { t.Fatalf("state %q, want %q; report %+v", st, provision.OfflineLanded, lw.report().Items) } r := lw.report() if ok, bad := r.ItemsOK(moves.OfflineOwnerLanding); !ok || r.Phase != moves.OfflinePhaseLanded { t.Fatalf("phase %s, not OK: %v (%+v)", r.Phase, bad, r.Items) } if len(r.Items) != len(moves.OfflineOwned(moves.OfflineOwnerLanding)) { t.Fatalf("report carries %d items, want every landing item", len(r.Items)) } if !reflect.DeepEqual(r.Staged, lw.seal.Staged) { t.Fatalf("the report's verified listing %v, the seal's %v", keysOf(r.Staged), keysOf(lw.seal.Staged)) } rt := lw.uk.row(offHost) if rt == nil || rt.Status != "landing" || rt.HoldOp != offID { t.Fatalf("target row %+v", rt) } lw.uk.edges() if lw.uk.lists(offHost) { t.Fatal("the edges file lists the host before open") } lw.requireNoTargetLease() } // requireNoTargetLease: nothing has written under data-uk instances/ but, // at most, a lifted tombstone (a stale one the landing lifted), and the // host's lock is free. func (lw *landWorld) requireNoTargetLease() { lw.t.Helper() loc := activator.HibernatePrefix(sealPrefix, offHost) for k := range lw.s3.current(lw.t, sealUK, "instances/") { if k != loc+"/lock.json" { lw.t.Fatalf("data-uk holds %s before the first wake", k) } } _, owner, expires, err := lease.LockState(context.Background(), sealUK, loc, sealUKRegion) if err != nil || !(owner == "" || lease.IsLiftedTombstone(owner, expires)) { lw.t.Fatalf("the target's lock is %q until %v (%v)", owner, expires, err) } } func keysOf[V any](m map[string]V) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } // tree is every regular file and symlink under dir with its content's // sha256, every directory with its mode: what a pass may not change. The // registry is left out (its file changes on every reconcile). func tree(t *testing.T, dir string) map[string]string { t.Helper() out := map[string]string{} err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } rel, _ := filepath.Rel(dir, p) if strings.HasPrefix(rel, "registry.db") { return nil } info, err := d.Info() if err != nil { return err } switch { case d.IsDir(): out[rel+"/"] = info.Mode().String() case info.Mode().IsRegular(): b, err := os.ReadFile(p) if err != nil { return err } sum := sha256.Sum256(b) out[rel] = hex.EncodeToString(sum[:]) + " " + info.Mode().String() default: out[rel] = info.Mode().String() } return nil }) if err != nil { t.Fatal(err) } return out } // requireSourceBytes: the target's files directory, env file and config // file are the source's byte for byte. A source with no files directory // lands an empty one. func (lw *landWorld) requireSourceBytes() { t := lw.t t.Helper() dst := tree(t, filepath.Join(lw.uk.dd, offHost)) if lw.opts.noFiles { if len(dst) != 1 || dst["./"] == "" { t.Fatalf("a source with no files directory landed %v", dst) } } else if src := tree(t, lw.srcFiles); !reflect.DeepEqual(src, dst) { t.Fatalf("files differ:\nsource %v\ntarget %v", src, dst) } for src, name := range map[string]string{lw.srcEnv: offHost + ".env", lw.srcConfig: offHost + ".config.env"} { a, err := os.ReadFile(src) if err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(lw.uk.dd, name)) if err != nil { t.Fatalf("%s on the target: %v", name, err) } if !bytes.Equal(a, b) { t.Fatalf("%s differs:\nsource %q\ntarget %q", name, a, b) } } } func itemsSummary(r moves.OfflineReport) map[string]moves.Check { return r.Items } func (lw *landWorld) parked(role string) map[string]string { dir := moves.OfflineParkDir(lw.uk.dd, offHost, offID, role) if !isDir(dir) { return nil } return tree(lw.t, dir) } // crashAt makes the target's landing crash once at point. func (lw *landWorld) crashAt(point string) *int { crashed := 0 lw.uk.w.cfg.landFault = func(p string) error { if p == point && crashed == 0 { crashed++ return errors.New("crash") } return nil } return &crashed } // --- (a) and (f): the landing -------------------------------------------------- // The full landing reaches offline-landed with every landing item OK, the // row held, nothing served and nothing taking the target's lease; and the // lesson of the 2026-10-02 outage: the target's .env, and so its // instance key, is the source's byte for byte, and its rendered // .config.env carries the app's store binding, its secret included. func TestOfflineLandHappyPath(t *testing.T) { lw := newLandWorld(t) lw.wd.flip() lw.uk.reconcile() if rt := lw.uk.row(offHost); rt == nil || rt.Status != "landing" || rt.HoldOp != offID { t.Fatalf("the target row before the landing: %+v", rt) } lw.uk.edges() lw.uk.pass() lw.requireLanded() lw.requireSourceBytes() // The instance key the app would be handed, byte for byte. read := func(b *offBox, name string) map[string]string { body, err := os.ReadFile(filepath.Join(b.dd, name)) if err != nil { t.Fatal(err) } return instancekey.ParseEnv(body) } srcKey, _ := instancekey.Effective(read(lw.ie, offHost+".config.env"), read(lw.ie, offHost+".env")) dstKey, layer := instancekey.Effective(read(lw.uk, offHost+".config.env"), read(lw.uk, offHost+".env")) if srcKey != landKeyValue || dstKey != srcKey || layer != instancekey.LayerOwn { t.Fatalf("instance key: source %q, target %q (layer %v)", srcKey, dstKey, layer) } // The store binding, as the target will launch it. env, _, _, err := lw.ukEB.LaunchLayers(*lw.uk.row(offHost)) if err != nil { t.Fatal(err) } for k, want := range map[string]string{"STORE_BUCKET": landStore, "STORE_ENDPOINT": "https://store.example.com", "STORE_SECRET_KEY": landSecret, "CARLOS_INSTANCE_KEY": landKeyValue, "CARLOS_ADMIN_TOKEN": "token"} { if env[k] != want { t.Fatalf("the target launches with %s=%q, want %q", k, env[k], want) } } // The ack names the move; the staged replica is untouched. ack := appconfig.ConfigAck{} body, _, err := lw.wd.s.Get(context.Background(), appconfig.ConfigAckKey("uk-box", offHost)) if err != nil || json.Unmarshal(body, &ack) != nil || ack.MoveOp != offID { t.Fatalf("config ack %+v %v", ack, err) } staged := lw.s3.current(t, "data-uk", activator.MovesPrefix(offHost)) if len(staged) != len(lw.seal.Staged) { t.Fatalf("staged %v, sealed %v", keysOf(staged), keysOf(lw.seal.Staged)) } // Timers, NextDue and replication arrived. rows, err := lw.uk.reg.TimersFor(offHost) if err != nil || moves.TimersDigest(rows) != lw.seal.TimersDigest || len(rows) != 2 { t.Fatalf("timers %+v %v", rows, err) } if rt := lw.uk.row(offHost); rt.NextDue != 1_800_000_000 || rt.Replication != registry.ReplicationBatched { t.Fatalf("row columns %+v", rt) } if lw.parked(moves.OfflineParkTargetBefore) != nil || lw.parked(moves.OfflineParkVerify) != nil { t.Fatal("a clean landing parked something or left its verify scratch") } } // --- (c) B2: every pass is an ensure --------------------------------------------- // A second, third and post-CAS pass, and a pass after an agent restart, // change nothing on disk, write nothing to the staged replica or the // archive, and report the same items. func TestOfflineLandIsAnEnsure(t *testing.T) { lw := newLandWorld(t) lw.flipAndLand(1) lw.requireLanded() disk := tree(t, lw.uk.dd) items := itemsSummary(lw.report()) data := lw.s3.current(t, "data-uk", "moves/") for i := range 4 { if i == 2 { lw.restart() } lw.uk.tick() lw.requireLanded() if got := tree(t, lw.uk.dd); !reflect.DeepEqual(got, disk) { t.Fatalf("pass %d changed the disk:\nbefore %v\nafter %v", i, disk, got) } if got := itemsSummary(lw.report()); !reflect.DeepEqual(got, items) { t.Fatalf("pass %d reports %+v, the first %+v", i, got, items) } if got := lw.s3.current(t, "data-uk", "moves/"); !reflect.DeepEqual(got, data) { t.Fatalf("pass %d changed data-uk moves/", i) } } } // A crash at every point of the landing (after each item, after the report, // after the CAS), followed by an agent restart, converges: the next passes // land everything, park nothing, and leave the disk as a clean landing does. func TestOfflineLandConvergesAfterACrashAtEveryPoint(t *testing.T) { var points []string for _, id := range []string{"warm-cache", "key-adoption", "lease", "db", "files", "config", "env", "env-pin", "timers", "next-due", "replication", "release", "report", "landed"} { points = append(points, "offline-"+id) } for _, point := range points { t.Run(point, func(t *testing.T) { lw := newLandWorld(t) crashed := 0 lw.uk.w.cfg.landFault = func(p string) error { if p == point && crashed == 0 { crashed++ return errors.New("crash") } return nil } lw.flipAndLand(1) if crashed != 1 { t.Fatalf("the crash point %s was never reached", point) } lw.restart() lw.uk.tick() lw.uk.tick() lw.requireLanded() lw.requireSourceBytes() if p := lw.parked(moves.OfflineParkTargetBefore); p != nil { t.Fatalf("parked its own landing: %v", p) } }) } } // --- (b) missing or corrupted items: mended ----------------------------------------- // Each item the landing carries, removed or damaged after the landing, is // put back by the next pass: a missing one landed, a different one parked // into target-before and landed again. The report stays landed and the // record offline-landed. func TestOfflineLandMendsWhatIsMissing(t *testing.T) { cases := []struct { name string damage func(lw *landWorld) parked []string // names that must appear in target-before }{ {"files directory removed", func(lw *landWorld) { os.RemoveAll(filepath.Join(lw.uk.dd, offHost)) }, nil}, {"a file changed", func(lw *landWorld) { writeFile(lw.t, filepath.Join(lw.uk.dd, offHost, "instance.key"), "tampered\n") }, []string{offHost + "/instance.key"}}, {"a file added", func(lw *landWorld) { writeFile(lw.t, filepath.Join(lw.uk.dd, offHost, "stray"), "x") }, []string{offHost + "/stray"}}, {"env removed", func(lw *landWorld) { os.Remove(filepath.Join(lw.uk.dd, offHost+".env")) }, []string{offHost + "/instance.key"}}, {"env changed: a new instance key", func(lw *landWorld) { writeFile(lw.t, filepath.Join(lw.uk.dd, offHost+".env"), "CARLOS_INSTANCE_KEY=minted-on-the-target\n") }, []string{offHost + ".env", offHost + "/instance.key"}}, {"config removed", func(lw *landWorld) { os.Remove(filepath.Join(lw.uk.dd, offHost+".config.env")) }, nil}, {"config changed", func(lw *landWorld) { writeFile(lw.t, filepath.Join(lw.uk.dd, offHost+".config.env"), "STORE_BUCKET=elsewhere\n") }, nil}, {"a timer removed", func(lw *landWorld) { if err := lw.uk.reg.ReplaceTimers(offHost, nil); err != nil { lw.t.Fatal(err) } }, nil}, {"next due changed", func(lw *landWorld) { lw.uk.reg.SetNextDue(offHost, 5) }, nil}, {"replication changed", func(lw *landWorld) { lw.uk.reg.SetReplication(offHost, registry.ReplicationDaily) }, nil}, {"a local database appeared", func(lw *landWorld) { writeFile(lw.t, provision.DefaultDBPath(lw.uk.dd, offHost), "stale") writeFile(lw.t, filepath.Join(activatorMetaDir(provision.DefaultDBPath(lw.uk.dd, offHost)), "ltx", "0", "x"), "meta") }, []string{offHost + ".db", ".activator-" + offHost + ".db-litestream/ltx/0/x"}}, {"an adoption record appeared", func(lw *landWorld) { writeFile(lw.t, filepath.Join(lw.uk.dd, offHost+instancekey.AdoptionSuffix), "{}") }, []string{offHost + instancekey.AdoptionSuffix}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { lw := newLandWorld(t) lw.flipAndLand(1) lw.requireLanded() c.damage(lw) lw.uk.tick() lw.requireLanded() lw.requireSourceBytes() if rows, _ := lw.uk.reg.TimersFor(offHost); moves.TimersDigest(rows) != lw.seal.TimersDigest { t.Fatal("timers not mended") } if rt := lw.uk.row(offHost); rt.NextDue != lw.seal.NextDue || rt.Replication != lw.seal.Replication { t.Fatalf("row columns not mended: %+v", rt) } parked := lw.parked(moves.OfflineParkTargetBefore) for _, name := range c.parked { if _, ok := parked[name]; !ok { t.Fatalf("%s not parked; target-before holds %v", name, keysOf(parked)) } } if len(c.parked) == 0 && parked != nil { t.Fatalf("parked %v", keysOf(parked)) } // And it stays mended: the next pass parks nothing more. before := tree(t, lw.uk.dd) lw.uk.tick() if got := tree(t, lw.uk.dd); !reflect.DeepEqual(got, before) { t.Fatal("the pass after the mend changed the disk") } }) } } // What a target holds for the host before the landing (an old residence's // database, meta dir, adoption record, files and env) is parked, never // trusted; a stale tombstone of another move in the target's bucket is // lifted; and the landing completes. func TestOfflineLandParksAndLiftsWhatItFinds(t *testing.T) { lw := newLandWorld(t) ctx := context.Background() dd := lw.uk.dd db := provision.DefaultDBPath(dd, offHost) writeFile(t, db, "old residence") writeFile(t, db+"-wal", "wal") writeFile(t, filepath.Join(activatorMetaDir(db), "ltx", "0", "0000000000000009-0000000000000009.ltx"), "meta") writeFile(t, filepath.Join(dd, offHost+instancekey.AdoptionSuffix), "{}") writeFile(t, filepath.Join(dd, offHost, "old.txt"), "old residence's file") writeFile(t, filepath.Join(dd, offHost+".env"), "CARLOS_INSTANCE_KEY=old-residence-key\n") loc := activator.HibernatePrefix("instances", offHost) if err := lease.Tombstone(ctx, "data-uk", loc, "eu-west-2", moves.TombstoneOwner(offID2), time.Hour); err != nil { t.Fatal(err) } lw.flipAndLand(1) lw.requireLanded() lw.requireSourceBytes() parked := lw.parked(moves.OfflineParkTargetBefore) for _, name := range []string{offHost + ".db", offHost + ".db-wal", ".activator-" + offHost + ".db-litestream/ltx/0/0000000000000009-0000000000000009.ltx", offHost + instancekey.AdoptionSuffix, offHost + "/old.txt", offHost + ".env"} { if _, ok := parked[name]; !ok { t.Fatalf("%s not parked: %v", name, keysOf(parked)) } } _, owner, expires, err := lease.LockState(ctx, "data-uk", loc, "eu-west-2") if err != nil || owner != moves.TombstoneOwner(offID2) || !lease.IsLiftedTombstone(owner, expires) { t.Fatalf("the stale tombstone: %q %v %v", owner, expires, err) } // Lifted, not taken: Acquire would now succeed, and nothing else // appeared under instances/. if got := keysOf(lw.s3.current(t, "data-uk", "instances/")); len(got) != 1 || got[0] != loc+"/lock.json" { t.Fatalf("data-uk instances/: %v", got) } } // --- (b) mismatches: failed, named -------------------------------------------------- // Every mismatch with the seal fails its own item in a failed report, and // the record stays offline-landing: an extra staged object, a changed one, // a changed archive, a changed bundle, a promoted channel, a seal that is // not the record's, a changed timers snapshot, a live writer's lease, and a // tombstone of this very move in the target's bucket. func TestOfflineLandFailsEachMismatch(t *testing.T) { prefix := moves.OfflineArchivePrefix(offHost, offID) cases := []struct { name string item string hard bool damage func(lw *landWorld) }{ {"an extra staged object", "db", true, func(lw *landWorld) { lw.s3.f.Put("data-uk", activator.MovesPrefix(offHost)+"0000/0000000000000009-0000000000000009.ltx", []byte("x")) }}, {"a changed staged object", "db", true, func(lw *landWorld) { k := keysOf(lw.seal.Staged)[0] lw.s3.f.Put("data-uk", activator.MovesPrefix(offHost)+k, []byte("not the sealed bytes")) }}, {"a staged object gone", "db", true, func(lw *landWorld) { lw.s3.f.BareDelete("data-uk", activator.MovesPrefix(offHost)+keysOf(lw.seal.Staged)[0]) }}, {"a changed archive", "files", true, func(lw *landWorld) { b, _ := lw.s3.f.Current("data-uk", prefix+"files.sealed") b = append([]byte(nil), b...) b[len(b)/2] ^= 0xff lw.s3.f.Put("data-uk", prefix+"files.sealed", b) }}, {"a changed bundle", "env-pin", true, func(lw *landWorld) { lw.bundle("shop-store-somewhere-else") }}, {"a promoted channel", "release", true, func(lw *landWorld) { lw.promote("v2") }}, {"a changed timers snapshot", "timers", true, func(lw *landWorld) { snap, _ := moves.TimersSnapshot{Host: offHost, TakenAt: lw.wd.clock()}.Encode() lw.s3.f.Put("data-uk", prefix+"timers.json", snap) }}, {"a live writer holds the target's lease", "lease", false, func(lw *landWorld) { held, err := lease.Acquire(context.Background(), "data-uk", activator.HibernatePrefix("instances", offHost), "eu-west-2") if err != nil { lw.t.Fatal(err) } lw.t.Cleanup(func() { held.Release(context.Background()) }) }}, {"this move's own tombstone in the target's bucket", "lease", true, func(lw *landWorld) { if err := lease.Tombstone(context.Background(), "data-uk", activator.HibernatePrefix("instances", offHost), "eu-west-2", moves.TombstoneOwner(offID), time.Hour); err != nil { lw.t.Fatal(err) } }}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { lw := newLandWorld(t) c.damage(lw) lw.flipAndLand(2) if st := lw.state(); st != provision.OfflineLanding { t.Fatalf("state %s", st) } r := lw.report() if r.Items[c.item].OK || r.Items[c.item].Reason == "" { t.Fatalf("%s: %+v", c.item, r.Items[c.item]) } if want := map[bool]string{true: moves.OfflinePhaseFailed, false: moves.OfflinePhaseLanded}[c.hard]; r.Phase != want { t.Fatalf("phase %s, want %s (%+v)", r.Phase, want, r.Items) } if rt := lw.uk.row(offHost); rt == nil || rt.HoldOp != offID { t.Fatalf("row %+v", rt) } lw.uk.edges() if lw.uk.lists(offHost) { t.Fatal("listed") } }) } // A seal whose bytes are not the ones the record names fails every item // after the row; nothing is landed. t.Run("a seal that is not the record's", func(t *testing.T) { lw := newLandWorld(t) if err := moves.PutReplace(context.Background(), lw.wd.s, moves.OfflineSealKey(offHost, offID), append(lw.sealRaw, ' ')); err != nil { t.Fatal(err) } lw.flipAndLand(1) r := lw.report() if r.Phase != moves.OfflinePhaseFailed || !r.Items["row"].OK { t.Fatalf("%+v", r) } for _, it := range moves.OfflineOwned(moves.OfflineOwnerLanding) { if it.ID != "row" && r.Items[it.ID].OK { t.Fatalf("%s OK under a seal that is not the record's", it.ID) } } if isDir(filepath.Join(lw.uk.dd, offHost)) || lexists(filepath.Join(lw.uk.dd, offHost+".env")) { t.Fatal("landed files under a seal that is not the record's") } }) // A mismatch that clears (the channel promoted back) lands on a later // pass: the report is recomputed from what is there every pass. t.Run("a mismatch that clears", func(t *testing.T) { lw := newLandWorld(t) lw.promote("v2") lw.flipAndLand(1) if r := lw.report(); r.Phase != moves.OfflinePhaseFailed || r.Items["release"].OK { t.Fatalf("%+v", r.Items["release"]) } lw.promote(sealVersion) lw.uk.tick() lw.requireLanded() }) } // --- (d) open --------------------------------------------------------------------- // open releases the hold only once every landing item verifies again // against the seal: a promote after the landing keeps the row held; once // it is undone the hold is released, the row is ready and listed, the // archive is deleted from the target's bucket, and the report says opened. // A following ExecBackend.Restore promotes the staged replica and its // database passes CheckDB with every row; until that first wake nothing has // taken the target's lease. func TestOfflineLandOpenReleasesOnlyWhenEveryItemVerifies(t *testing.T) { lw := newLandWorld(t) ctx := context.Background() // A fresh target: no live link for the host, which the release item // accepts (nothing can run until adoption stages the sealed build). if lexists(release.LiveLink(lw.uk.opt, offHost)) { t.Fatal("fixture: the target already has a live link") } lw.flipAndLand(1) lw.requireLanded() lw.wd.open() // An item that no longer verifies: not opened. lw.promote("v2") lw.uk.tick() if rt := lw.uk.row(offHost); rt == nil || rt.HoldOp != offID || rt.Status != "landing" { t.Fatalf("released with the release item failing: %+v", rt) } if r := lw.report(); r.Phase == moves.OfflinePhaseOpened || r.Items["release"].OK { t.Fatalf("report at open: %s %+v", r.Phase, r.Items["release"]) } lw.requireNoTargetLease() // An item the open's ensure mends (a timer lost) does not block it. lw.promote(sealVersion) if err := lw.uk.reg.ReplaceTimers(offHost, nil); err != nil { t.Fatal(err) } lw.uk.tick() rt := lw.uk.row(offHost) if rt == nil || rt.Status != "ready" || rt.HoldOp != "" { t.Fatalf("not opened: %+v (%+v)", rt, lw.report().Items) } if rows, _ := lw.uk.reg.TimersFor(offHost); moves.TimersDigest(rows) != lw.seal.TimersDigest { t.Fatal("timers not mended before the release") } r := lw.report() if r.Phase != moves.OfflinePhaseOpened { t.Fatalf("phase %s", r.Phase) } if ok, bad := r.ItemsOK(moves.OfflineOwnerLanding); !ok { t.Fatalf("opened report: %v", bad) } lw.uk.edges() if !lw.uk.lists(offHost) { t.Fatal("the edges file does not list the opened host") } if got := lw.s3.current(t, "data-uk", moves.OfflineArchivePrefix(offHost, offID)); len(got) != 0 { t.Fatalf("the archive is still there: %v", keysOf(got)) } lw.requireNoTargetLease() lw.requireSourceBytes() // The first wake's restore: staging promoted into data-uk's own prefix, // the database restored and checked. m, err := lease.NewS3Mirror(ctx, "eu-west-2", "eu-west-2") if err != nil { t.Fatal(err) } eb := &activator.ExecBackend{Cfg: activator.Config{Bucket: "data-uk", Region: "eu-west-2", Prefix: "instances"}, DataDir: lw.uk.dd, OptDir: lw.uk.opt, Mirror: m} fresh, err := eb.Restore(ctx, *rt) if err != nil || fresh { t.Fatalf("restore: fresh %v, %v", fresh, err) } if err := lease.CheckDB(rt.DBPath); err != nil { t.Fatal(err) } if n := countRows(t, rt.DBPath); n != 20 { t.Fatalf("restored %d rows, want 20", n) } if got := lw.s3.current(t, "data-uk", activator.MovesPrefix(offHost)); len(got) != 0 { t.Fatalf("staging left after the promotion: %v", keysOf(got)) } // Later passes at open keep the report and do not re-verify the // staging the wake consumed. lw.uk.tick() lw.restart() lw.uk.tick() if r := lw.report(); r.Phase != moves.OfflinePhaseOpened { t.Fatalf("phase after the wake: %s", r.Phase) } if rt := lw.uk.row(offHost); rt.Status != "ready" { t.Fatalf("row %+v", rt) } } // --- (e) the target's revert ---------------------------------------------------------- // An abort after the flip parks what the target landed (files, env, config) // under the move's target park directory, deletes the held row and its // timers, reports reverted, and leaves data-uk instances// untouched; // a later pass changes nothing. func TestOfflineLandTargetRevert(t *testing.T) { lw := newLandWorld(t) lw.flipAndLand(1) lw.requireLanded() landed := tree(t, filepath.Join(lw.uk.dd, offHost)) lw.wd.abort() lw.uk.tick() if rt := lw.uk.row(offHost); rt != nil { t.Fatalf("the row is still there: %+v", rt) } if rows, _ := lw.uk.reg.TimersFor(offHost); len(rows) != 0 { t.Fatalf("timers left: %+v", rows) } for _, name := range []string{offHost, offHost + ".env", offHost + ".config.env"} { if lexists(filepath.Join(lw.uk.dd, name)) { t.Fatalf("%s left in place", name) } } parkDir := moves.OfflineParkDir(lw.uk.dd, offHost, offID, moves.OfflineRoleTarget) if got := tree(t, filepath.Join(parkDir, offHost)); !reflect.DeepEqual(got, landed) { t.Fatalf("parked files %v, landed %v", got, landed) } for _, name := range []string{offHost + ".env", offHost + ".config.env"} { if !lexists(filepath.Join(parkDir, name)) { t.Fatalf("%s not parked", name) } } if r := lw.report(); r.Phase != moves.OfflinePhaseReverted || r.RowPresent { t.Fatalf("report %+v", r) } lw.requireNoTargetLease() if lw.state() != provision.OfflineReverting { t.Fatalf("state %s", lw.state()) } disk := tree(t, lw.uk.dd) lw.restart() lw.uk.tick() lw.uk.tick() if rt := lw.uk.row(offHost); rt != nil { t.Fatalf("the row came back: %+v", rt) } if got := tree(t, lw.uk.dd); !reflect.DeepEqual(got, disk) { t.Fatal("a later revert pass changed the disk") } lw.uk.edges() if lw.uk.lists(offHost) { t.Fatal("listed after the revert") } } // An abort after the flip on a target that never landed: the held row // reconcile minted is deleted, nothing is parked, and a target that never // had a row does nothing at all. func TestOfflineLandTargetRevertBeforeTheLanding(t *testing.T) { lw := newLandWorld(t) // The target mints its row held and runs its moves pass, which crashes // before anything is landed or reported. lw.uk.w.cfg.landFault = func(p string) error { if p == "offline-warm-cache" { return errors.New("crash") } return nil } lw.flipAndLand(1) if rt := lw.uk.row(offHost); rt == nil || rt.HoldOp != offID { t.Fatalf("row %+v", rt) } if _, ok := lw.wd.reportIf(moves.OfflineRoleTarget, "uk-box"); ok { t.Fatal("reported before the crash point") } lw.wd.abort() lw.restart() lw.uk.tick() if rt := lw.uk.row(offHost); rt != nil { t.Fatalf("row %+v", rt) } if isDir(moves.OfflineParkDir(lw.uk.dd, offHost, offID, moves.OfflineRoleTarget)) { t.Fatal("parked something it never landed") } if r := lw.report(); r.Phase != moves.OfflinePhaseReverted { t.Fatalf("report %+v", r) } lw2 := newLandWorld(t) lw2.wd.flip() lw2.wd.abort() lw2.uk.tick() if rt := lw2.uk.row(offHost); rt != nil { t.Fatalf("row %+v", rt) } if _, ok := lw2.wd.reportIf(moves.OfflineRoleTarget, "uk-box"); ok { t.Fatal("a target that never had a row reported") } } // The instance key the app is handed may come from the app's configuration // rather than .env (instancekey.Effective). The env item judges the key // the target would hand over, from both files as the activator merges them: // a configured key lands; a configured key changed after the seal fails the // env item by name (and the env pin), so the target never hands the app a key // that is not the source's. func TestOfflineLandConfiguredInstanceKey(t *testing.T) { lw := newLandWorldWith(t, landOpts{configKey: landKeyValue}) lw.flipAndLand(1) lw.requireLanded() lw.requireSourceBytes() env, _, _, err := lw.ukEB.LaunchLayers(*lw.uk.row(offHost)) if err != nil || env[instancekey.Var] != landKeyValue { t.Fatalf("the target hands over %q (%v)", env[instancekey.Var], err) } lw2 := newLandWorldWith(t, landOpts{configKey: landKeyValue}) lw2.opts.configKey = "ik_ffffffffffffffffffffffffffffffff" lw2.bundle(landStore) lw2.flipAndLand(2) if st := lw2.state(); st != provision.OfflineLanding { t.Fatalf("state %s", st) } r := lw2.report() if c := r.Items["env"]; c.OK || !strings.Contains(c.Reason, "instance key") || r.Phase != moves.OfflinePhaseFailed { t.Fatalf("env %+v, phase %s", c, r.Phase) } if r.Items["env-pin"].OK { t.Fatal("the env pin passed with another key") } } // Review I1: a first landing pass that extracted the files and env and // rendered the config, then crashed before it could report, followed by an // abort: the revert parks all three while the row is still held, before it // deletes the row, so nothing of the move's is left in the data directory. func TestOfflineLandRevertParksAfterACrashBeforeTheReport(t *testing.T) { lw := newLandWorld(t) lw.crashAt("offline-release") lw.flipAndLand(1) if _, ok := lw.wd.reportIf(moves.OfflineRoleTarget, "uk-box"); ok { t.Fatal("reported before the crash point") } names := []string{offHost, offHost + ".env", offHost + ".config.env"} for _, name := range names { if !lexists(filepath.Join(lw.uk.dd, name)) { t.Fatalf("fixture: %s was not landed before the crash", name) } } landed := tree(t, filepath.Join(lw.uk.dd, offHost)) lw.wd.abort() lw.restart() lw.uk.tick() parkDir := moves.OfflineParkDir(lw.uk.dd, offHost, offID, moves.OfflineRoleTarget) for _, name := range names { if lexists(filepath.Join(lw.uk.dd, name)) { t.Fatalf("%s left in the data directory", name) } if !lexists(filepath.Join(parkDir, name)) { t.Fatalf("%s not parked", name) } } if got := tree(t, filepath.Join(parkDir, offHost)); !reflect.DeepEqual(got, landed) { t.Fatalf("parked %v, landed %v", got, landed) } if rt := lw.uk.row(offHost); rt != nil { t.Fatalf("row %+v", rt) } if r := lw.report(); r.Phase != moves.OfflinePhaseReverted { t.Fatalf("report %+v", r) } } // Review I2: a failed env pin names the variables that differ, from the // source's env report beside the archive, and never their values. func TestOfflineLandEnvPinNamesTheVariables(t *testing.T) { lw := newLandWorld(t) lw.bundle("shop-store-somewhere-else") lw.flipAndLand(1) c := lw.report().Items["env-pin"] if c.OK || !strings.Contains(c.Reason, "STORE_BUCKET") || strings.Contains(c.Reason, "somewhere-else") || strings.Contains(c.Reason, landStore) { t.Fatalf("env-pin %+v", c) } for _, name := range []string{"STORE_ENDPOINT", "STORE_SECRET_KEY", "FEATURE", "CARLOS_REGION"} { if strings.Contains(c.Reason, name) { t.Fatalf("env-pin names %s, which did not change: %q", name, c.Reason) } } // Without the source's report the pin still fails, by digest alone. lw2 := newLandWorld(t) lw2.bundle("shop-store-somewhere-else") lw2.s3.f.BareDelete(sealUK, moves.OfflineArchivePrefix(offHost, offID)+offlineEnvReportObject) lw2.flipAndLand(1) if c := lw2.report().Items["env-pin"]; c.OK || c.Reason != "the launch environment differs from the source's" { t.Fatalf("env-pin without the source's report %+v", c) } } // Review M1: the verified report is written before the release, so a crash // after the release leaves the items that pass verified (here after an // earlier open pass failed), and the next pass reports opened with every // item OK. func TestOfflineLandOpenAfterACrashPastTheRelease(t *testing.T) { lw := newLandWorld(t) lw.flipAndLand(1) lw.requireLanded() lw.wd.open() lw.promote("v2") lw.uk.tick() if r := lw.report(); r.Items["release"].OK { t.Fatal("fixture: the release item passed") } lw.promote(sealVersion) crashed := lw.crashAt("offline-released") lw.uk.tick() if *crashed != 1 { t.Fatal("the crash point was never reached") } if rt := lw.uk.row(offHost); rt == nil || rt.Status != "ready" { t.Fatalf("row %+v", rt) } if r := lw.report(); r.Phase == moves.OfflinePhaseOpened { t.Fatal("fixture: reported opened before the crash") } else if ok, bad := r.ItemsOK(moves.OfflineOwnerLanding); !ok { t.Fatalf("the report before the release carries %v not OK", bad) } lw.restart() lw.uk.tick() r := lw.report() if ok, bad := r.ItemsOK(moves.OfflineOwnerLanding); r.Phase != moves.OfflinePhaseOpened || !ok { t.Fatalf("phase %s, not OK %v", r.Phase, bad) } if got := lw.s3.current(t, sealUK, moves.OfflineArchivePrefix(offHost, offID)); len(got) != 0 { t.Fatalf("the archive is still there: %v", keysOf(got)) } } // T4's contract for a host with no files directory: the archive carries no // files/ entry and requires no file. The landing accepts it: extraction // leaves an empty / (0755), whose digest with the env file is // the seal's, and later passes leave it alone. func TestOfflineLandAHostWithNoFilesDirectory(t *testing.T) { lw := newLandWorldWith(t, landOpts{noFiles: true}) if lw.seal.FilesEntries != 1 { t.Fatalf("fixture: the seal carries %d entries, want the env alone", lw.seal.FilesEntries) } lw.flipAndLand(1) lw.requireLanded() lw.requireSourceBytes() st, err := os.Stat(filepath.Join(lw.uk.dd, offHost)) if err != nil || st.Mode().Perm() != 0o755 { t.Fatalf("the files directory: %v %v", st, err) } disk := tree(t, lw.uk.dd) lw.uk.tick() lw.requireLanded() if got := tree(t, lw.uk.dd); !reflect.DeepEqual(got, disk) { t.Fatal("a later pass changed the disk") } } // A nested source keeps launch descriptors (launch..json and // launch.gen..json) beside its files directory. They are the platform's, // not carried, and the landing neither expects nor makes them: it lands the // files directory alone, flat. func TestOfflineLandANestedSourceWithLaunchFiles(t *testing.T) { lw := newLandWorldWith(t, landOpts{nested: true}) lw.flipAndLand(1) lw.requireLanded() lw.requireSourceBytes() for rel := range tree(t, lw.uk.dd) { if strings.Contains(filepath.Base(rel), "launch.") { t.Fatalf("the target holds %s", rel) } } } // --- the target's own live build (final review, return moves) -------------------- const landOlderBuild = "the target box still holds an older build of this instance. see the offline move runbook" // stageLive gives the target box a live link for the host naming version v // of the app, as a box the instance once left keeps it after retirement: a // staged executable in the version store and the link to it. func (lw *landWorld) stageLive(v string) string { t := lw.t t.Helper() writeFile(t, release.VersionPath(lw.uk.opt, lw.wd.acct, v, "app"), "#!/bin/sh\nexit 0\n") if err := release.PointLive(lw.uk.opt, offHost, lw.wd.acct, v, "app"); err != nil { t.Fatal(err) } target, err := os.Readlink(release.LiveLink(lw.uk.opt, offHost)) if err != nil { t.Fatal(err) } return target } // requireNoWake: the row is still held landing by the move, the edges file // does not list it (so the edge never hands it to the activator), and a // scheduled tick for it, the one wake source that reaches a held row, is // answered with the maintenance page without reaching the exec backend. func (lw *landWorld) requireNoWake(when string) { t := lw.t t.Helper() rt := lw.uk.row(offHost) if rt == nil || rt.Status != "landing" || rt.HoldOp != offID { t.Fatalf("%s: the target row %+v is not held by the move", when, rt) } lw.uk.edges() if lw.uk.lists(offHost) { t.Fatalf("%s: the edges file lists the host", when) } proxied := false rec := httptest.NewRecorder() lw.uk.svc.ServeScheduled(rec, httptest.NewRequest("POST", "http://"+offHost+"/jobs/remind", nil), *rt, http.HandlerFunc(func(http.ResponseWriter, *http.Request) { proxied = true })) if proxied || rec.Code != http.StatusServiceUnavailable { t.Fatalf("%s: a scheduled tick on the target was answered %d (proxied %v)", when, rec.Code, proxied) } if rt := lw.uk.row(offHost); rt == nil || rt.Status != "landing" || rt.HoldOp != offID { t.Fatalf("%s: after the tick the target row is %+v", when, rt) } } // A return move after an upgrade: the target box is one the instance left // on an older build, and it still keeps that build's live link (retirement // leaves the root-owned link). The channel names the sealed build, so only // the link can tell. The landing fails the release item with MV-O177 and // every other item lands; the record stays offline-landing; nothing can // wake the row; the link is left exactly as it was. Open, reached even so, // verifies the item again and keeps the hold. A link that names the sealed // build passes, and open then releases the row. func TestOfflineLandRefusesAnOlderLiveBuildOnTheTarget(t *testing.T) { lw := newLandWorld(t) old := lw.stageLive("v0") lw.flipAndLand(2) if st := lw.state(); st != provision.OfflineLanding { t.Fatalf("state %s with an older live build on the target", st) } r := lw.report() if r.Phase != moves.OfflinePhaseFailed || r.Items["release"].OK || r.Items["release"].Reason != landOlderBuild { t.Fatalf("phase %s, release %+v", r.Phase, r.Items["release"]) } for id, c := range r.Items { if id != "release" && !c.OK { t.Fatalf("%s not OK: %+v", id, c) } } lw.requireNoWake("after the landing") if got, err := os.Readlink(release.LiveLink(lw.uk.opt, offHost)); err != nil || got != old { t.Fatalf("the live link is %q (%v), want it left at %q", got, err, old) } // Open, forced past the door's landed gate, verifies again and refuses. lw.wd.landed() lw.wd.open() lw.uk.tick() lw.uk.tick() if r := lw.report(); r.Phase == moves.OfflinePhaseOpened || r.Items["release"].Reason != landOlderBuild { t.Fatalf("open with an older live build: %s %+v", r.Phase, r.Items["release"]) } lw.requireNoWake("at open") lw.requireNoTargetLease() // The same link naming the sealed build: open releases the row. lw.stageLive(sealVersion) lw.uk.tick() if rt := lw.uk.row(offHost); rt == nil || rt.Status != "ready" || rt.HoldOp != "" { t.Fatalf("not opened with the sealed live build: %+v (%+v)", rt, lw.report().Items) } if r := lw.report(); r.Phase != moves.OfflinePhaseOpened { t.Fatalf("phase %s", r.Phase) } } // A live link that names another build fails at open too: the landing was // clean (no link), and the link appears before open. func TestOfflineLandOpenRefusesALiveBuildThatAppeared(t *testing.T) { lw := newLandWorld(t) lw.flipAndLand(1) lw.requireLanded() lw.stageLive("v0") lw.wd.open() lw.uk.tick() if r := lw.report(); r.Phase == moves.OfflinePhaseOpened || r.Items["release"].Reason != landOlderBuild { t.Fatalf("open with another live build: %s %+v", r.Phase, r.Items["release"]) } lw.requireNoWake("at open") }