package cluster import ( "context" "testing" ) type fakeHandler struct { load Load claim func(ctx context.Context, tk *Task) error } func (h *fakeHandler) RuntimeLoad() Load { return h.load } func (h *fakeHandler) Claim(ctx context.Context, tk *Task) error { if h.claim != nil { return h.claim(ctx, tk) } return nil } // sendNull is a no-op sender (single-node ring). func sendNull(ctx context.Context, next string, tk *Token) error { return nil } // TestSingleNodeCycle: leader starts ring, processes phases locally, and the // task gets claimed (lowest load = only node). func TestSingleNodeCycle(t *testing.T) { eng := NewEngine("n1", "n1:7500", "u", "p", "0.71.0", []string{"0.71.0"}, &fakeHandler{load: Load{MemPct: 30, NetPct: 30}}, sendNull, "n1:7500", true) eng.StartRing(context.Background()) // single node: token stays local; no successor so nothing travels. if eng.State().Nodes[0].ID != "n1" { t.Fatalf("nodes = %+v", eng.State().Nodes) } } // TestTwoNodesPhaseCollect: leader n1 sends to n2; n1 collects both after n2 // stamps; then phase flips and second round syncs. func TestTwoNodesCollectAndSync(t *testing.T) { var n2loaded bool eng2 := NewEngine("n2", "n2:7500", "u", "p", "0.71.0", nil, &fakeHandler{load: Load{MemPct: 10, NetPct: 10}}, sendNull, "n2:7500", false) // n2's state must include both (leader + itself) so AliveSuccessor works eng2.state.UpsertNode(Node{ID: "n1", Addr: "n1:7500", Alive: true, IsLeader: true, Load: Load{MemPct: 50, NetPct: 50}}) // n1 sends a collect-phase token with itself in Passed. eng2.state.UpsertNode(Node{ID: "n1", Addr: "n1:7500", Alive: true, IsLeader: true, Load: eng2.state.Nodes[0].Load}) tk := &Token{Cycle: 1, Phase: PhaseCollect, Passed: []string{"n1"}, State: *func() *State { s := &State{} s.UpsertNode(Node{ID: "n1", Addr: "n1:7500", Alive: true, IsLeader: true}) s.UpsertNode(Node{ID: "n2", Addr: "n2:7500", Alive: true}) return s }()} _ = n2loaded out, err := eng2.OnToken(context.Background(), tk) if err != nil { t.Fatal(err) } if !contains(out.Passed, "n2") { t.Fatalf("n2 not stamped in round1, passed=%v", out.Passed) } }