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