package main import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func TestCoordinatorResetsBarrierWhenConnectionCountDrops(t *testing.T) { state := &coordinatorState{ start: StartInfo{RunID: "run-reset", Scenario: "group"}, startDelay: time.Minute, commitDelay: time.Second, expectedAgents: 2, ready: make(map[string]AgentReady), armed: make(map[string]bool), ledgers: make(map[string][]AcceptedMessage), summaries: make(map[string]AgentSummary), complete: make(chan struct{}), } postReady := func(ready AgentReady) StartInfo { raw, err := json.Marshal(ready) if err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, "/v1/ready", bytes.NewReader(raw)) w := httptest.NewRecorder() state.handleReady(w, req) if w.Code != http.StatusOK { t.Fatalf("ready status=%d body=%s", w.Code, w.Body.String()) } var start StartInfo if err = json.Unmarshal(w.Body.Bytes(), &start); err != nil { t.Fatal(err) } return start } fullA := AgentReady{Machine: "a", ReadyConnections: 1000, ExpectedConnections: 1000} fullB := AgentReady{Machine: "b", ReadyConnections: 1000, ExpectedConnections: 1000} postReady(fullA) if start := postReady(fullB); start.StartAt.IsZero() { t.Fatal("full agents must create a validation barrier") } fullA.ReadyConnections = 999 if start := postReady(fullA); !start.StartAt.IsZero() || start.Committed { t.Fatalf("connection drop must reset barrier: %+v", start) } } func TestCoordinatorClientWaitsForBothAgentsAndMergesLedger(t *testing.T) { state := &coordinatorState{ start: StartInfo{RunID: "run1", Scenario: "p2p"}, startDelay: 50 * time.Millisecond, commitDelay: 50 * time.Millisecond, expectedAgents: 2, ready: make(map[string]AgentReady), ledgers: make(map[string][]AcceptedMessage), armed: make(map[string]bool), summaries: make(map[string]AgentSummary), complete: make(chan struct{}), } server := httptest.NewServer(testCoordinatorMux(state)) defer server.Close() client := newCoordinatorClient(server.URL) aDone := make(chan StartInfo, 1) go func() { start, _ := client.ready(context.Background(), func() AgentReady { return AgentReady{Machine: "a", ReadyConnections: 1000, ExpectedConnections: 1000} }) aDone <- start }() time.Sleep(50 * time.Millisecond) startB, err := client.ready(context.Background(), func() AgentReady { return AgentReady{Machine: "b", ReadyConnections: 1000, ExpectedConnections: 1000} }) if err != nil || startB.StartAt.IsZero() || !startB.Committed { t.Fatalf("machine B ready: start=%+v err=%v", startB, err) } select { case startA := <-aDone: if !startA.StartAt.Equal(startB.StartAt) { t.Fatalf("different start times A=%s B=%s", startA.StartAt, startB.StartAt) } case <-time.After(2 * time.Second): t.Fatal("machine A did not receive start barrier") } if err = client.submitLedger(context.Background(), "a", []AcceptedMessage{{ClientMid: "b"}}); err != nil { t.Fatal(err) } if err = client.submitLedger(context.Background(), "b", []AcceptedMessage{{ClientMid: "a"}}); err != nil { t.Fatal(err) } ledger, err := client.ledger(context.Background()) if err != nil || len(ledger) != 2 || ledger[0].ClientMid != "a" || ledger[1].ClientMid != "b" { t.Fatalf("ledger=%+v err=%v", ledger, err) } if err = client.submitObserved(context.Background(), "a", []AcceptedMessage{{ClientMid: "unknown", Seq: 10}}); err != nil { t.Fatal(err) } if err = client.submitObserved(context.Background(), "b", []AcceptedMessage{{ClientMid: "unknown", Seq: 10}}); err != nil { t.Fatal(err) } observed, err := client.observed(context.Background()) if err != nil || len(observed) != 1 || observed[0].ClientMid != "unknown" { t.Fatalf("observed=%+v err=%v", observed, err) } if err = client.submitHistory(context.Background(), "a", []string{"mid-2", "mid-1"}); err != nil { t.Fatal(err) } if err = client.submitHistory(context.Background(), "b", []string{"mid-2", "mid-3"}); err != nil { t.Fatal(err) } history, err := client.history(context.Background()) if err != nil || len(history) != 3 || history[0] != "mid-1" || history[1] != "mid-2" || history[2] != "mid-3" { t.Fatalf("history=%+v err=%v", history, err) } } func testCoordinatorMux(state *coordinatorState) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/v1/ready", state.handleReady) mux.HandleFunc("/v1/start", state.handleStart) mux.HandleFunc("/v1/ledger", state.handleLedger) mux.HandleFunc("/v1/observed", state.handleObserved) mux.HandleFunc("/v1/history", state.handleHistory) mux.HandleFunc("/v1/summary", state.handleSummary) mux.HandleFunc("/v1/status", state.handleStatus) return mux }