package hub import ( "encoding/json" "os" "reflect" "sort" "testing" ) // The host-report shape is a contract DUPLICATED across two repos (no shared types // module yet). testdata/host-report.golden.json MUST be kept byte-identical with // felhom-hub's hub/internal/api/testdata/host-report.golden.json. This test fails // if a json tag on HostReport/HostMetrics/Guest is renamed/added/removed relative // to the golden, catching silent drift before slices 5/6 populate the empty // collections. (Promote to a shared types module when those land.) func TestHostReport_ContractMatchesGolden(t *testing.T) { raw, err := os.ReadFile("testdata/host-report.golden.json") if err != nil { t.Fatal(err) } var golden map[string]any if err := json.Unmarshal(raw, &golden); err != nil { t.Fatalf("golden is not valid JSON: %v", err) } // A constructed report mirroring the golden's populated shape (guests[0] has spec). report := &HostReport{ HostID: "demo-host-01", ReportedAt: "2026-06-08T12:00:00Z", AgentVersion: "0.3.1", Host: HostMetrics{Node: "demo-felhom", LoadAvg: []string{"0.10"}}, Guests: []Guest{ {VMID: 100, Name: "a", Status: "running", ControllerVersion: "", Spec: &GuestSpec{Cores: 2}}, {VMID: 101, Name: "b", Status: "stopped", ControllerVersion: ""}, }, StorageTargets: []StorageTarget{}, Backups: []Backup{}, RestoreTests: []RestoreTest{}, PBSSnapshots: []PBSSnapshot{}, AuditTail: []AuditEntry{}, Cloudflared: Cloudflared{Status: "active"}, } b, _ := json.Marshal(report) var got map[string]any json.Unmarshal(b, &got) assertSameKeys(t, "", golden, got) assertSameKeys(t, "host", golden["host"], got["host"]) assertSameKeys(t, "guests[0]", firstElem(golden["guests"]), firstElem(got["guests"])) } func firstElem(v any) any { arr, ok := v.([]any) if !ok || len(arr) == 0 { return map[string]any{} } return arr[0] } func assertSameKeys(t *testing.T, where string, a, b any) { t.Helper() ka, kb := keysOf(a), keysOf(b) if !reflect.DeepEqual(ka, kb) { t.Errorf("contract drift at %s:\n golden keys = %v\n struct keys = %v", where, ka, kb) } } func keysOf(v any) []string { m, ok := v.(map[string]any) if !ok { return nil } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys }