package web import ( "context" "reflect" "sync" "testing" "time" ) // R-705 (v0.279.0) — the manual night chain runs dump → Tier 2 → off-site → update leg, IN ORDER, one at a // time; it is refused while anything else runs and while a chain runs; with no off-site target it skips // only that leg. COMPANION RED-PROOF: swap two step calls in run() → the order assertion fails; drop the // busy() check in start() → the refusal assertion fails. func TestR705_NightChainRunsTheLegsInOrderAndRefusesWhenBusy(t *testing.T) { var mu sync.Mutex var order []string rec := func(n string) { mu.Lock(); order = append(order, n); mu.Unlock() } release := make(chan struct{}) done := make(chan struct{}) c := nightChain{ dump: func(context.Context) error { rec("db-dump"); <-release; return nil }, tier2: func() { rec("tier2") }, offsite: func(context.Context) error { rec("offsite"); return nil }, leg: func(context.Context) { rec("update-leg"); close(done) }, busy: func() (bool, string) { return false, "" }, logf: func(string, ...interface{}) {}, } ok, why, legs := c.start() if !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "offsite", "update-leg"}) { t.Fatalf("start: ok=%v why=%q legs=%v", ok, why, legs) } // A second press while the first chain runs is refused. if ok, why, _ := c.start(); ok || why != "the night's chain is already running" { t.Fatalf("second start while running: ok=%v why=%q", ok, why) } close(release) select { case <-done: case <-time.After(5 * time.Second): t.Fatal("the chain never reached the update leg") } for i := 0; i < 100 && nightChainRunning.Load(); i++ { time.Sleep(10 * time.Millisecond) } if want := []string{"db-dump", "tier2", "offsite", "update-leg"}; !reflect.DeepEqual(order, want) { t.Fatalf("order %v, want %v", order, want) } // Busy: refused, nothing runs. order = nil c.busy = func() (bool, string) { return true, "a guarded update is running" } if ok, why, _ := c.start(); ok || why != "a guarded update is running" { t.Fatalf("busy start: ok=%v why=%q", ok, why) } time.Sleep(50 * time.Millisecond) if len(order) != 0 { t.Fatalf("a refused chain ran legs: %v", order) } // No off-site target: that leg alone is skipped. c.busy = func() (bool, string) { return false, "" } c.offsite = nil c.dump = func(context.Context) error { rec("db-dump"); return nil } done2 := make(chan struct{}) c.leg = func(context.Context) { rec("update-leg"); close(done2) } if ok, _, legs := c.start(); !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "update-leg"}) { t.Fatalf("no-offsite start: ok=%v legs=%v", ok, legs) } <-done2 for i := 0; i < 100 && nightChainRunning.Load(); i++ { time.Sleep(10 * time.Millisecond) } if want := []string{"db-dump", "tier2", "update-leg"}; !reflect.DeepEqual(order, want) { t.Fatalf("no-offsite order %v, want %v", order, want) } }