package commandcenter import ( "bytes" "encoding/json" "reflect" "testing" ) // TestNormalizeListsLeavesNoNilSlice pins the contract that every list field is emitted as a LIST. // // Go marshals a nil slice to JSON null, the client maps straight over these fields, and null.map throws // during render, so one nil here is a blank Command Center for any tenant that happens to have none of // that thing. A brand new tenant has none of most of them. // // Reflection rather than eight hand written assertions, deliberately: a list field added to View later is // then caught by this test instead of by whichever customer first has an empty one. func TestNormalizeListsLeavesNoNilSlice(t *testing.T) { var v View // the zero value is exactly the empty tenant case: every slice nil normalizeLists(&v) // Recursive, because the nulls are not only at the top level: meta.notes, mission_readiness.drivers, // the four proposal_readiness lists, win_impact.top and action_center.launch are all nested, and the // client maps over several of them exactly like the top level ones. checked := 0 assertNoNilSlice(t, reflect.ValueOf(v), "View", &checked) if checked < 8 { t.Fatalf("only %d slice fields walked; the view has more than that, so the walk is not reaching them", checked) } // A nil POINTER is deliberately still allowed. action_center.suggested_requirement_id being null means // "there is no suggestion", which is a real answer and different from an empty list; the client's // optional handling depends on that distinction. Only lists must never be null. b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } for _, listKey := range []string{ "active_opportunities", "staffing_gaps", "labor_category_coverage", "heat_map", "upcoming_opportunities", "incumbent_landscape", "ai_recommendations", "executive_cards", "notes", "drivers", "flags", "top", "launch", "alerts", } { if bytes.Contains(b, []byte(`"`+listKey+`":null`)) { t.Errorf("%q marshalled as null; the client will throw on .map", listKey) } } } func assertNoNilSlice(t *testing.T, rv reflect.Value, path string, checked *int) { t.Helper() switch rv.Kind() { case reflect.Struct: for i := 0; i < rv.NumField(); i++ { if rv.Type().Field(i).IsExported() { assertNoNilSlice(t, rv.Field(i), path+"."+rv.Type().Field(i).Name, checked) } } case reflect.Slice: *checked++ if rv.IsNil() { t.Errorf("%s is nil after normalizeLists: it marshals as JSON null and the client throws on .map", path) } case reflect.Ptr: if !rv.IsNil() { assertNoNilSlice(t, rv.Elem(), path, checked) } } }