Standardize neon cyan on #00e8f5, wire SacredPageHeader across Builds/Path Tracer/LOTL, dedupe Pages.css, align Path Tracer chrome, drop dead wealth-deck CSS, add prefers-color-scheme shell + HelpTip, sidebar version from package.json build inject, and Vitest CSS regression guards.
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package miner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
)
|
|
|
|
func TestDefaultFallbackChainAutoWithDocker(t *testing.T) {
|
|
SetRuntimeDetector(func() ContainerRuntimeInfo {
|
|
return ContainerRuntimeInfo{Available: true, CLI: "docker"}
|
|
})
|
|
defer SetRuntimeDetector(nil)
|
|
SetWSLDetector(func() WSLRuntimeInfo { return WSLRuntimeInfo{} })
|
|
defer SetWSLDetector(nil)
|
|
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: ExecutionAuto,
|
|
PoolHost: "pool.example.com",
|
|
},
|
|
}, ContainerRuntimeInfo{Available: true, CLI: "docker"})
|
|
|
|
want := []MiningMethod{MethodContainer, MethodInProcess, MethodStratumDirect}
|
|
if len(chain) < len(want) {
|
|
t.Fatalf("chain=%v want at least %v", chain, want)
|
|
}
|
|
for i := range want {
|
|
if chain[i] != want[i] {
|
|
t.Fatalf("chain[%d]=%q want %q full=%v", i, chain[i], want[i], chain)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultFallbackChainAutoWithoutRuntime(t *testing.T) {
|
|
SetWSLDetector(func() WSLRuntimeInfo { return WSLRuntimeInfo{} })
|
|
defer SetWSLDetector(nil)
|
|
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto, PoolHost: "p"},
|
|
}, ContainerRuntimeInfo{})
|
|
|
|
if chain[0] != MethodInProcess {
|
|
t.Fatalf("got %v want inprocess first", chain)
|
|
}
|
|
// Windows LOTL probe tiers trail stratum_direct in the legacy chain.
|
|
stratumIdx := -1
|
|
for i, m := range chain {
|
|
if m == MethodStratumDirect {
|
|
stratumIdx = i
|
|
}
|
|
}
|
|
if stratumIdx < 0 {
|
|
t.Fatalf("got %v want stratum_direct present", chain)
|
|
}
|
|
if runtime.GOOS != "windows" && chain[len(chain)-1] != MethodStratumDirect {
|
|
t.Fatalf("got %v want stratum last", chain)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFallbackChainInProcessSkipsContainer(t *testing.T) {
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: ExecutionInProcess,
|
|
GPUEnabled: true,
|
|
RVNWallet: "wallet",
|
|
PoolHost: "p",
|
|
},
|
|
}, ContainerRuntimeInfo{Available: true, CLI: "docker"})
|
|
|
|
for _, m := range chain {
|
|
if m == MethodContainer {
|
|
t.Fatal("inprocess mode must skip container")
|
|
}
|
|
}
|
|
if chain[0] != MethodInProcess {
|
|
t.Fatalf("got %v", chain)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFallbackChainGPUIncludedWhenConfigured(t *testing.T) {
|
|
SetWSLDetector(func() WSLRuntimeInfo { return WSLRuntimeInfo{} })
|
|
defer SetWSLDetector(nil)
|
|
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: ExecutionAuto,
|
|
GPUEnabled: true,
|
|
RVNWallet: "wallet",
|
|
},
|
|
}, ContainerRuntimeInfo{Available: true, CLI: "docker"})
|
|
|
|
foundGPU := false
|
|
for _, m := range chain {
|
|
if m == MethodGPUSubprocess {
|
|
foundGPU = true
|
|
}
|
|
}
|
|
if !foundGPU {
|
|
t.Fatalf("expected gpu in chain, got %v", chain)
|
|
}
|
|
}
|
|
|
|
func TestTryChainOrderAndSkipMissingRuntime(t *testing.T) {
|
|
var started []MiningMethod
|
|
hooks := ChainHooks{
|
|
StartContainer: func() error {
|
|
started = append(started, MethodContainer)
|
|
return errors.New("container start blocked")
|
|
},
|
|
StartInProcess: func() error {
|
|
started = append(started, MethodInProcess)
|
|
return nil
|
|
},
|
|
StartGPU: func() error { return nil },
|
|
IsGPUSupported: func() bool { return false },
|
|
}
|
|
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: ExecutionAuto,
|
|
PoolHost: "p",
|
|
},
|
|
}, hooks, nil)
|
|
ctrl.runtime = ContainerRuntimeInfo{Available: true, CLI: "docker"}
|
|
ctrl.chain = DefaultFallbackChain(ctrl.cfg, ctrl.runtime)
|
|
|
|
method, err := ctrl.TryChain(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TryChain: %v", err)
|
|
}
|
|
if method != MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess", method)
|
|
}
|
|
if len(started) != 2 || started[0] != MethodContainer || started[1] != MethodInProcess {
|
|
t.Fatalf("start order=%v", started)
|
|
}
|
|
if len(ctrl.Status().FailedMethods) != 1 || ctrl.Status().FailedMethods[0].Method != MethodContainer {
|
|
t.Fatalf("failures=%v", ctrl.Status().FailedMethods)
|
|
}
|
|
}
|
|
|
|
func TestTryChainExhausted(t *testing.T) {
|
|
hooks := ChainHooks{
|
|
StartContainer: func() error { return errors.New("no container") },
|
|
StartInProcess: func() error { return errors.New("no inprocess") },
|
|
}
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionInProcess},
|
|
}, hooks, nil)
|
|
ctrl.chain = []MiningMethod{MethodInProcess}
|
|
|
|
_, err := ctrl.TryChain(context.Background())
|
|
if !errors.Is(err, ErrChainExhausted) && err.Error() != "no inprocess" {
|
|
t.Fatalf("got err=%v", err)
|
|
}
|
|
if !ctrl.Status().ChainExhausted {
|
|
t.Fatal("expected chain exhausted flag")
|
|
}
|
|
}
|
|
|
|
func TestAdvancePrimaryFromContainer(t *testing.T) {
|
|
var inprocessStarted bool
|
|
hooks := ChainHooks{
|
|
StartInProcess: func() error {
|
|
inprocessStarted = true
|
|
return nil
|
|
},
|
|
StopContainer: func() {},
|
|
}
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto},
|
|
}, hooks, nil)
|
|
ctrl.chain = []MiningMethod{MethodContainer, MethodInProcess}
|
|
ctrl.activePrimary = MethodContainer
|
|
|
|
ctrl.AdvancePrimary("container exited")
|
|
if !inprocessStarted {
|
|
t.Fatal("expected inprocess start after container failure")
|
|
}
|
|
if ctrl.Status().ActiveMethod != MethodInProcess {
|
|
t.Fatalf("active=%q", ctrl.Status().ActiveMethod)
|
|
}
|
|
}
|
|
|
|
func TestDefaultChainCooldownConstant(t *testing.T) {
|
|
if DefaultChainCooldown != 30*time.Second {
|
|
t.Fatalf("DefaultChainCooldown = %v want 30s", DefaultChainCooldown)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFallbackChainForcedContainerWithoutRuntime(t *testing.T) {
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: ExecutionContainer,
|
|
PoolHost: "p",
|
|
},
|
|
}, ContainerRuntimeInfo{})
|
|
|
|
for _, m := range chain {
|
|
if m == MethodContainer {
|
|
t.Fatal("container mode without runtime must skip container method")
|
|
}
|
|
}
|
|
if chain[0] != MethodInProcess {
|
|
t.Fatalf("got %v", chain)
|
|
}
|
|
}
|
|
|
|
func TestDefaultFallbackChainDockerLoadBeforeContainer(t *testing.T) {
|
|
t.Setenv("AETHERFORGE_DOCKER_IMAGE_TAR", "")
|
|
SetImageTarFetcher(func(cfg config.RuntimeConfig) (string, error) {
|
|
return "/policy/worker.tar", nil
|
|
})
|
|
defer SetImageTarFetcher(nil)
|
|
SetRuntimeDetector(func() ContainerRuntimeInfo {
|
|
return ContainerRuntimeInfo{Available: true, CLI: "docker"}
|
|
})
|
|
defer SetRuntimeDetector(nil)
|
|
SetWSLDetector(func() WSLRuntimeInfo { return WSLRuntimeInfo{} })
|
|
defer SetWSLDetector(nil)
|
|
|
|
chain := DefaultFallbackChain(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto, PoolHost: "p"},
|
|
}, ContainerRuntimeInfo{Available: true, CLI: "docker"})
|
|
|
|
if chain[0] != MethodDockerLoad {
|
|
t.Fatalf("chain=%v want docker_load first when tar policy set", chain)
|
|
}
|
|
if chain[1] != MethodContainer {
|
|
t.Fatalf("chain=%v want container second", chain)
|
|
}
|
|
}
|
|
|
|
func TestTryChainDockerLoadFailsReportsAttempt(t *testing.T) {
|
|
SetImageTarFetcher(func(cfg config.RuntimeConfig) (string, error) {
|
|
return "/tmp/worker.tar", nil
|
|
})
|
|
defer SetImageTarFetcher(nil)
|
|
|
|
var failed []MiningMethod
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto, PoolHost: "p"},
|
|
}, ChainHooks{
|
|
StartDockerLoad: func() error { failed = append(failed, MethodDockerLoad); return errors.New("docker missing") },
|
|
StartInProcess: func() error { return nil },
|
|
}, nil)
|
|
ctrl.runtime = ContainerRuntimeInfo{Available: true, CLI: "docker"}
|
|
ctrl.chain = []MiningMethod{MethodDockerLoad, MethodInProcess}
|
|
|
|
method, err := ctrl.TryChain(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TryChain: %v", err)
|
|
}
|
|
if method != MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess", method)
|
|
}
|
|
if len(ctrl.Status().FailedMethods) != 1 || ctrl.Status().FailedMethods[0].Method != MethodDockerLoad {
|
|
t.Fatalf("failures=%v", ctrl.Status().FailedMethods)
|
|
}
|
|
}
|
|
|
|
func TestTryChainRunTierHooksPopulatesLOTLFields(t *testing.T) {
|
|
hooks := ChainHooks{
|
|
StartInProcess: func() error { return nil },
|
|
RunTierProbes: func() TierReport {
|
|
return TierReport{
|
|
Attempts: []TierAttempt{{
|
|
Tier: TierWebView2Probe,
|
|
OK: true,
|
|
Wallet: "same-wallet",
|
|
}},
|
|
WebGPUReady: true,
|
|
}
|
|
},
|
|
RunTierChain: func() (LOTLTier, error) {
|
|
return TierCPUInprocess, nil
|
|
},
|
|
}
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionInProcess},
|
|
}, hooks, nil)
|
|
ctrl.chain = []MiningMethod{MethodInProcess}
|
|
|
|
if _, err := ctrl.TryChain(context.Background()); err != nil {
|
|
t.Fatalf("TryChain: %v", err)
|
|
}
|
|
st := ctrl.Status()
|
|
if st.LOTLTier != TierCPUInprocess {
|
|
t.Fatalf("lotl_tier=%q want cpu_inprocess", st.LOTLTier)
|
|
}
|
|
if len(st.LOTLAttempts) != 1 || st.LOTLAttempts[0].Tier != TierWebView2Probe {
|
|
t.Fatalf("lotl_attempts=%v", st.LOTLAttempts)
|
|
}
|
|
if !st.WebGPUReady {
|
|
t.Fatal("expected webgpu_ready from tier probes")
|
|
}
|
|
}
|
|
|
|
func TestOnMethodFailedReporterIncludesFailedMethods(t *testing.T) {
|
|
var events []string
|
|
var last MiningStatus
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionAuto},
|
|
}, ChainHooks{}, func(status MiningStatus, eventType string) {
|
|
events = append(events, eventType)
|
|
last = status
|
|
})
|
|
|
|
ctrl.OnMethodFailed(MethodContainer, "docker blocked")
|
|
if len(last.FailedMethods) != 1 {
|
|
t.Fatalf("failed_methods=%v", last.FailedMethods)
|
|
}
|
|
if last.FailedMethods[0].Method != MethodContainer || last.FailedMethods[0].Reason != "docker blocked" {
|
|
t.Fatalf("failure=%+v", last.FailedMethods[0])
|
|
}
|
|
if last.FailedMethods[0].At == "" {
|
|
t.Fatal("expected RFC3339 timestamp on failure")
|
|
}
|
|
if len(events) != 1 || events[0] != "mining_fallback" {
|
|
t.Fatalf("events=%v", events)
|
|
}
|
|
}
|
|
|
|
func TestRestartChainBypassesCooldown(t *testing.T) {
|
|
attempts := 0
|
|
hooks := ChainHooks{
|
|
StartInProcess: func() error {
|
|
attempts++
|
|
return nil
|
|
},
|
|
}
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionInProcess},
|
|
}, hooks, nil)
|
|
ctrl.chain = []MiningMethod{MethodInProcess}
|
|
|
|
if _, err := ctrl.TryChain(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ctrl.TryChain(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("attempts=%d want 1 before restart", attempts)
|
|
}
|
|
|
|
ctrl.RestartChain(context.Background())
|
|
if attempts != 2 {
|
|
t.Fatalf("attempts=%d want 2 after RestartChain clears cooldown", attempts)
|
|
}
|
|
}
|
|
|
|
func TestTryChainRespectsCooldown(t *testing.T) {
|
|
attempts := 0
|
|
hooks := ChainHooks{
|
|
StartInProcess: func() error {
|
|
attempts++
|
|
return nil
|
|
},
|
|
}
|
|
ctrl := NewChainController(config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{MinerExecution: ExecutionInProcess},
|
|
}, hooks, nil)
|
|
ctrl.chain = []MiningMethod{MethodInProcess}
|
|
|
|
if _, err := ctrl.TryChain(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ctrl.TryChain(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("attempts=%d want 1 (cooldown)", attempts)
|
|
}
|
|
}
|