package server import ( "fmt" "testing" "time" "vdi-cloud/platform/internal/store" ) func TestTerminalMetricsIdentityValidationAndCompatibility(t *testing.T) { f := newFixtureWorker(t, false) viewer := f.login("viewer") for _, id := range []string{"metric-a", "metric-b"} { _, err := f.db.Exec(`INSERT INTO terminals(id,sn,name,token_hash,bootstrap_hash,last_seen,status) VALUES($1,$1,$1,$2,'',now(),'online')`, id, store.Hash(id+"-token")) if err != nil { t.Fatal(err) } } body := map[string]any{"agentVersion": "qt-platform/0.4.0", "terminalId": "metric-b", "systemMetrics": map[string]any{"cpuPercent": 0, "memoryPercent": 30, "socTemperatureC": 44.37, "uptimeSeconds": 120}} status, out := f.request("POST", "/terminal/heartbeat", "metric-a-token", body, "") expect(t, 200, status, out) status, out = f.request("GET", "/terminals/metric-a/metrics", viewer, nil, "") expect(t, 200, status, out) latest := out["latest"].(map[string]any) if latest["supported"] != true || latest["fresh"] != true || latest["cpuPercent"] != float64(0) || latest["socTemperatureC"] != 44.37 { t.Fatal(latest) } status, out = f.request("GET", "/terminals/metric-b/metrics", viewer, nil, "") expect(t, 200, status, out) if out["latest"].(map[string]any)["supported"] != false { t.Fatal("forged terminal identity accepted", out) } // Repeated uploads update current state but cannot grow history without limit. status, out = f.request("POST", "/terminal/heartbeat", "metric-a-token", body, "") expect(t, 200, status, out) var count int f.db.Get(&count, "SELECT count(*) FROM terminal_metric_samples WHERE terminal_id='metric-a'") if count != 1 { t.Fatal("unbounded duplicate samples", count) } // Session reports also advance last_seen; they must not hide a recent metric. f.db.Exec("UPDATE terminals SET last_seen=now() WHERE id='metric-a'") status, out = f.request("GET", "/terminals/metric-a/metrics", viewer, nil, "") expect(t, 200, status, out) if out["latest"].(map[string]any)["fresh"] != true { t.Fatal("session report hid metrics", out) } for _, invalid := range []map[string]any{{"cpuPercent": 101}, {"memoryPercent": -1}, {"socTemperatureC": 151}, {"uptimeSeconds": -0.1}, {"cpuPercent": "nan"}} { status, out = f.request("POST", "/terminal/heartbeat", "metric-a-token", map[string]any{"agentVersion": "test", "systemMetrics": invalid}, "") expect(t, 400, status, out) } f.db.Exec("UPDATE terminals SET last_seen=now()-interval '91 seconds' WHERE id='metric-a'") status, out = f.request("GET", "/terminals/metric-a/metrics", viewer, nil, "") expect(t, 200, status, out) latest = out["latest"].(map[string]any) if latest["fresh"] != false || latest["online"] != false || latest["cpuPercent"] != nil || latest["socTemperatureC"] != nil { t.Fatal("stale live value", latest) } status, out = f.request("POST", "/terminal/heartbeat", "metric-a-token", map[string]any{"agentVersion": "legacy"}, "") expect(t, 200, status, out) status, out = f.request("GET", "/terminals/metric-a/metrics", viewer, nil, "") expect(t, 200, status, out) latest = out["latest"].(map[string]any) if latest["supported"] != false || latest["cpuPercent"] != nil { t.Fatal("legacy values retained", latest) } admin := f.login("admin") status, out = f.request("POST", "/terminals/metric-a/disable", admin, nil, "") expect(t, 200, status, out) status, out = f.request("POST", "/terminal/heartbeat", "metric-a-token", body, "") expect(t, 401, status, out) status, out = f.request("POST", "/terminals/metric-a/reactivate", admin, nil, "") expect(t, 200, status, out) secret := out["activationSecret"] status, out = f.request("POST", "/terminal/register", "", map[string]any{"sn": "metric-a", "activationSecret": secret, "agentVersion": "test"}, "") expect(t, 200, status, out) status, out = f.request("GET", "/terminals/metric-a/metrics", viewer, nil, "") expect(t, 200, status, out) if out["latest"].(map[string]any)["supported"] != false || out["latest"].(map[string]any)["fresh"] != false { t.Fatal("old identity metrics retained", out) } status, out = f.request("GET", "/terminals/missing/metrics", viewer, nil, "") expect(t, 404, status, out) status, out = f.request("GET", "/terminals/metric-a/metrics", "metric-b-token", nil, "") expect(t, 401, status, out) status, out = f.request("GET", "/terminals/metric-a/metrics", "", nil, "") expect(t, 401, status, out) // A never-registered terminal has null last_seen and no current metrics. f.db.Exec("INSERT INTO terminals(id,sn,name,bootstrap_hash) VALUES('metric-new','metric-new','new','')") status, out = f.request("GET", "/terminals/metric-new/metrics", viewer, nil, "") expect(t, 200, status, out) if out["latest"].(map[string]any)["online"] != false { t.Fatal(out) } } func TestTerminalMetricsAggregationGapsAndRetention(t *testing.T) { f := newFixtureWorker(t, false) viewer := f.login("viewer") if _, err := f.db.Exec("INSERT INTO terminals(id,sn,name,bootstrap_hash) VALUES('metrics-series','metrics-series','series','')"); err != nil { t.Fatal(err) } start := time.Now().UTC().Add(-time.Hour).Truncate(time.Minute) for _, p := range []struct{ seconds, cpu int }{{10, 20}, {30, 40}, {130, 60}} { if _, err := f.db.Exec(`INSERT INTO terminal_metric_samples(terminal_id,sampled_at,cpu_percent) VALUES('metrics-series',$1,$2)`, start.Add(time.Duration(p.seconds)*time.Second), p.cpu); err != nil { t.Fatal(err) } } path := fmt.Sprintf("/terminals/metrics-series/metrics?from=%s&to=%s", start.Format(time.RFC3339), start.Add(3*time.Minute).Format(time.RFC3339)) status, out := f.request("GET", path, viewer, nil, "") expect(t, 200, status, out) points := out["items"].([]any) if len(points) != 4 { t.Fatal(out) } first, gap, third := points[0].(map[string]any), points[1].(map[string]any), points[2].(map[string]any) if first["cpuPercent"] != float64(30) || first["samples"] != float64(2) || first["memoryPercent"] != nil || gap["cpuPercent"] != nil || gap["samples"] != float64(0) || third["cpuPercent"] != float64(60) { t.Fatal(points) } status, out = f.request("GET", "/terminals/metrics-series/metrics?from=2026-01-01T00:00:00Z&to=2026-01-09T00:00:00Z", viewer, nil, "") expect(t, 400, status, out) status, out = f.request("GET", "/terminals/metrics-series/metrics?from=bad", viewer, nil, "") expect(t, 400, status, out) for _, hours := range []int{24, 168} { status, out = f.request("GET", fmt.Sprintf("/terminals/metrics-series/metrics?from=%s&to=%s", start.Add(-time.Duration(hours)*time.Hour).Format(time.RFC3339), start.Format(time.RFC3339)), viewer, nil, "") expect(t, 200, status, out) want := float64(300) if hours == 168 { want = 1800 } if out["bucketSeconds"] != want || len(out["items"].([]any)) > 400 { t.Fatal(out) } } f.db.Exec("INSERT INTO terminal_metric_samples(terminal_id,sampled_at) VALUES('metrics-series',now()-interval '31 days')") if err := f.server.observe(t.Context()); err != nil { t.Fatal(err) } var old int f.db.Get(&old, "SELECT count(*) FROM terminal_metric_samples WHERE sampled_at