package activator import ( "context" "errors" "slices" "testing" ) // Merge of zdd-flag-readiness (main's Task 9): on a box that declined // CARLOS_SWAP=overlap and honours failure records or hold-ran markers, the // live exec gate is asked about the build a live-link start is about to // run. On this branch a cold wake does not exec the live link: step 2b // captures the link once, records that build and runs the captured file by // path (StartGenExe, SeedExe, a worker's onto), and a sidecar on an // unstaged generation does the same. Main's backend checks sit on the // live-link arms only (exe == "", onto == nil), so the capture itself must // ask the gate, and before the record: a build the box will not start is // never named as the one about to run (settings spec §4.2), so a refusal // completes no request. // A cold instance wake whose live link names a held build runs nothing and // records nothing; one whose link names a build the gate passes is asked // about exactly that build and runs it. func TestTheDeclinedGateIsAskedAboutTheCapturedWakeBeforeItsRecord(t *testing.T) { a, b := identityActivator(t, "v2") b.restoreFresh = true l := &execLog{} a.BeforeExec = recordInto(b, l) g := &liveGate{refuse: map[string]bool{"v2": true}} a.SetLiveExecGate(g.gate) _, err := a.beginRequest(context.Background(), seeded(identityRoute(t, "one.example.com"))) if !errors.Is(err, errLiveHeld) { t.Fatalf("err = %v, want the gate's refusal", err) } if calls, _, _ := l.seen(); len(calls) != 0 { t.Fatalf("records %v: a build the box will not start was recorded", calls) } if b.startCount() != 0 || b.seedCount() != 0 { t.Fatalf("started %d, seeded %d: held v2 ran", b.startCount(), b.seedCount()) } if got := g.builds(); !slices.Equal(got, []string{"v2"}) { t.Fatalf("gate asked %v, want [v2]", got) } waitFor(t, "the lease to go back", func() bool { return b.releaseCount() == 1 }) b.setLive("v1") done, err := a.beginRequest(context.Background(), identityRoute(t, "one.example.com")) if err != nil { t.Fatalf("wake onto v1 = %v", err) } done() if got := g.builds(); !slices.Equal(got, []string{"v2", "v1"}) { t.Fatalf("gate asked %v, want [v2 v1]", got) } if got := identityEvents(b); !slices.Equal(got, []string{"record v1", "start v1"}) { t.Fatalf("events %v, want v1 recorded and started", got) } } // A worker wake execs its captured identity (onto), which the backend's // gate never asks about: step 2b asks it, before the record. func TestTheDeclinedGateIsAskedAboutTheCapturedWorkerWake(t *testing.T) { a, b, _, _, _ := stagedActivator(t) b.fakeBackend.setIdentity("v2") l := &execLog{} a.BeforeExec = l.hook g := &liveGate{refuse: map[string]bool{"v2": true}} a.SetLiveExecGate(g.gate) host := "bot.x.internal" if err := a.WakeWorker(alwaysOnWorkerRoute(host)); !errors.Is(err, errLiveHeld) { t.Fatalf("WakeWorker = %v, want the gate's refusal", err) } if got := b.calls(); len(got) != 0 { t.Fatalf("staged spawns %v: held v2 ran", got) } if calls, _, _ := l.seen(); len(calls) != 0 { t.Fatalf("records %v: a build the box will not start was recorded", calls) } if got := g.builds(); !slices.Equal(got, []string{"v2"}) { t.Fatalf("gate asked %v, want [v2]", got) } } // A sidecar whose generation names no staged file resolves the live link // and runs that file by path: the gate is asked about it first. When the // gate refuses the link's build (root repointed it at held v2 while the // instance kept serving v1), the sidecar runs the instance's own build // instead, once that build's staged copy is named (BuildIdentity) and the // gate passes it too (readiness 2 Task 5, recon A §3a): otherwise nothing // ever restarts it. With the instance's build refused as well, or its copy // unreadable, the refusal stands: nothing is recorded or started, and the // instance keeps serving. func TestTheDeclinedGateIsAskedAboutAnUnstagedSidecarsCapture(t *testing.T) { t.Parallel() host := "one.example.com" setup := func(t *testing.T, refuse map[string]bool, ownErr error) (*Service, *planGenBackend, *liveGate, *execLog) { t.Helper() a, b := sidecarActivator(t, "") done, err := a.beginRequest(context.Background(), identityRoute(t, host)) if err != nil { t.Fatal(err) } done() if gen, _ := a.entryGen(host); gen.StagedPath != "" || b.sidecarStartCount() != 1 { t.Fatalf("setup: generation %+v, %d live-link sidecars", gen, b.sidecarStartCount()) } // The instance serves v1 from a generation that names no staged // file (woken before this binary, or the recovery probe's). a.mu.Lock() a.entries[host].gen.Build = "v1" a.mu.Unlock() b.fakeBackend.setIdentity("v2") b.fakeBackend.mu.Lock() b.fakeBackend.ident.buildIDErr = ownErr b.fakeBackend.mu.Unlock() l := &execLog{} a.BeforeExec = l.hook g := &liveGate{refuse: refuse} a.SetLiveExecGate(g.gate) b.lastSidecar().kill() a.sweepOnce() return a, b, g, l } stranded := func(t *testing.T, a *Service, b *planGenBackend, g *liveGate, l *execLog, asked []string) { t.Helper() waitFor(t, "the refused restart", func() bool { return len(g.builds()) >= len(asked) && a.sidecarSettled(host) }) if sc := b.sidecarsOn(); len(sc) != 0 || b.sidecarStartCount() != 1 { t.Fatalf("sidecars %+v, %d live-link: a held build ran beside the instance", sc, b.sidecarStartCount()) } if calls, _, _ := l.seen(); len(calls) != 0 { t.Fatalf("records %v: a build the box will not start was recorded", calls) } if got := g.builds(); !slices.Equal(got, asked) { t.Fatalf("gate asked %v, want %v", got, asked) } if _, ok := a.entryGen(host); !ok { t.Fatal("the instance stopped serving over its sidecar's refusal") } } t.Run("the instance's own build runs", func(t *testing.T) { a, b, g, l := setup(t, map[string]bool{"v2": true}, nil) waitFor(t, "the sidecar on v1", func() bool { return len(b.sidecarsOn()) == 1 && a.sidecarSettled(host) }) own := fakeIdentity("v1") if sc := b.sidecarsOn()[0]; sc.Build != "v1" || sc.StagedPath != own.StagedPath || sc.Digest != own.Digest { t.Fatalf("sidecar on %+v, want the instance's own v1 (%+v)", sc, own) } if got := g.builds(); !slices.Equal(got, []string{"v2", "v1"}) { t.Fatalf("gate asked %v, want [v2 v1]", got) } if got := b.buildIdentities(); !slices.Equal(got, []string{"v1"}) { t.Fatalf("BuildIdentity asked %v, want [v1]", got) } if calls, _, _ := l.seen(); !slices.Equal(calls, []string{host + "@v1"}) { t.Fatalf("records %v, want v1 recorded before its sidecar ran", calls) } if b.sidecarStartCount() != 1 { t.Fatalf("%d live-link sidecars: the recovery used the live link", b.sidecarStartCount()) } }) t.Run("the instance's own build is held too", func(t *testing.T) { a, b, g, l := setup(t, map[string]bool{"v2": true, "v1": true}, nil) stranded(t, a, b, g, l, []string{"v2", "v1"}) }) t.Run("the instance's own copy cannot be named", func(t *testing.T) { a, b, g, l := setup(t, map[string]bool{"v2": true}, errors.New("staged copy unreadable")) stranded(t, a, b, g, l, []string{"v2"}) }) }