Files
AetherForge/server/internal/ai/court_prompt_test.go

155 lines
4.7 KiB
Go

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 TestBuildCourtPromptListsRetryCommands(t *testing.T) {
bundle := BuildCourtPrompt(AgentSnapshot{Stuck: true}, "", nil, PersonaBalanced)
if !strings.Contains(bundle.SystemPrompt, "spread_retry_lane") {
t.Fatalf("missing spread_retry_lane in court prompt: %s", bundle.SystemPrompt)
}
if !strings.Contains(bundle.SystemPrompt, "skip_tier") {
t.Fatalf("missing skip_tier in court prompt")
}
}
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)
}
}