package main import ( "context" "net" "testing" "time" "forge-mesh/internal/api/types" "forge-mesh/internal/mining" ) func startMockStratum(t *testing.T) (host, port string) { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = ln.Close() }) go func() { for { conn, err := ln.Accept() if err != nil { return } go func(c net.Conn) { defer c.Close() buf := make([]byte, 4096) _, _ = c.Read(buf) }(conn) } }() host, port, _ = net.SplitHostPort(ln.Addr().String()) return host, port } func TestMinerTierChainStateMachine(t *testing.T) { host, port := startMockStratum(t) profile := types.MiningProfile{ WalletAddress: "chain-wallet", Tiers: []types.MiningTierSpec{ {Type: "stratum", Duration: 0, Config: map[string]string{"algo": "rx/0"}}, {Type: "stratum", Duration: 0, Config: map[string]string{"algo": "rx/0"}}, }, } cfg := mining.DefaultChainConfig() cfg.HostID = "chain-host" cfg.StratumHost = host cfg.StratumXMRPort = port cfg.GateWindow = 100 * time.Millisecond cfg.PollInterval = 40 * time.Millisecond cfg.DefaultProbe = 800 * time.Millisecond cfg.MinHashrateHps = 500 chain := mining.NewChain(profile, cfg) ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() go func() { _ = chain.Run(ctx) }() seen := map[string]bool{} deadline := time.Now().Add(10 * time.Second) for time.Now().Before(deadline) { st := chain.Status() if st.TierState != "" { seen[st.TierState] = true } if st.CurrentTier >= 1 && st.HashrateHps >= cfg.MinHashrateHps && st.TierState == mining.TierStateActive { if st.Wallet != "chain-wallet" { t.Fatalf("wallet not pinned: %q", st.Wallet) } break } time.Sleep(80 * time.Millisecond) } chain.Stop() cancel() if !seen[mining.TierStateProbing] { t.Fatal("expected probing state in tier chain") } if !seen[mining.TierStateActive] { t.Fatal("expected active state during mock stratum tier") } st := chain.Status() if st.CurrentTier < 1 { t.Fatalf("expected tier progression, got tier %d", st.CurrentTier) } } func TestMinerTierChainMockHashrateGate(t *testing.T) { host, port := startMockStratum(t) profile := types.MiningProfile{ WalletAddress: "mock-wallet", Tiers: []types.MiningTierSpec{ {Type: "stratum", Duration: 0, Config: map[string]string{"algo": "rx/0"}}, }, } cfg := mining.DefaultChainConfig() cfg.HostID = "mock-host" cfg.StratumHost = host cfg.StratumXMRPort = port cfg.GateWindow = 50 * time.Millisecond cfg.PollInterval = 25 * time.Millisecond cfg.DefaultProbe = 2 * time.Second cfg.MinHashrateHps = 100 chain := mining.NewChain(profile, cfg) ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second) defer cancel() go func() { _ = chain.Run(ctx) }() deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { st := chain.Status() if st.HashrateHps >= cfg.MinHashrateHps && st.TierState == mining.TierStateActive { if !st.Simulated { t.Log("using real stratum path (non-simulated)") } chain.Stop() return } time.Sleep(50 * time.Millisecond) } t.Fatal("mock stratum tier did not reach hashrate gate") }