Add onion contingency miner tree with orchestrator and Seer telemetry.
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
Single-host branch runner complements fallback chain under Fleet AI Control: persona ghost forks, onion_miner_log hops, server exhaustion splice from graft mining genome, Crucible depth badge, and hospice retire after max cycles.
This commit is contained in:
171
agent/miner/contingency_tree_test.go
Normal file
171
agent/miner/contingency_tree_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user