155 lines
4.7 KiB
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)
|
|
}
|
|
}
|