## Sprint 28 — CharacterVisualDescriptor unit tests (#703) ## ## Validates the GDScript data contract for character visual state. ## Tests cover: BodyType enum (D-159), default fields, field mutation, ## body_type_key() helper, from_dict() / to_dict() round-trips, and ## edge cases (missing fields, unknown body_type wire string). ## ## The Rust side (server/src/bridge/types.rs) is out of scope for this ## client-team file — Rust tests live server-side. ## ## Spec: D-159 (11 body types), D-160 (21 segments), D-161 (head separate) ## Ticket: #703 class_name TestCharacterVisualDescriptorSprint28 extends GdUnitTestSuite # -- BodyType enum — 11 variants (D-159) -------------------------------------- func test_body_type_enum_has_11_variants() -> void: # D-159: 4 adult types × 2 genders + child = 11 total. var bt := CharacterVisualDescriptor.BodyType var variants := [ bt.THIN_M, bt.THIN_F, bt.AVERAGE_M, bt.AVERAGE_F, bt.MUSCULAR_M, bt.MUSCULAR_F, bt.TEEN_M, bt.TEEN_F, bt.HEAVY_M, bt.HEAVY_F, bt.CHILD, ] assert_int(variants.size()).override_failure_message( "BodyType enum must have exactly 11 variants per D-159" ).is_equal(11) func test_body_type_all_variants_exist() -> void: # Each variant must be accessible without raising an error var bt := CharacterVisualDescriptor.BodyType assert_bool(bt.THIN_M >= 0).is_true() assert_bool(bt.THIN_F >= 0).is_true() assert_bool(bt.AVERAGE_M >= 0).is_true() assert_bool(bt.AVERAGE_F >= 0).is_true() assert_bool(bt.MUSCULAR_M >= 0).is_true() assert_bool(bt.MUSCULAR_F >= 0).is_true() assert_bool(bt.TEEN_M >= 0).is_true() assert_bool(bt.TEEN_F >= 0).is_true() assert_bool(bt.HEAVY_M >= 0).is_true() assert_bool(bt.HEAVY_F >= 0).is_true() assert_bool(bt.CHILD >= 0).is_true() # -- BODY_TYPE_KEYS lookup coverage ------------------------------------------- func test_body_type_keys_covers_all_variants() -> void: # BODY_TYPE_KEYS must have an entry for every BodyType variant var bt := CharacterVisualDescriptor.BodyType var keys := CharacterVisualDescriptor.BODY_TYPE_KEYS for variant in [bt.THIN_M, bt.THIN_F, bt.AVERAGE_M, bt.AVERAGE_F, bt.MUSCULAR_M, bt.MUSCULAR_F, bt.TEEN_M, bt.TEEN_F, bt.HEAVY_M, bt.HEAVY_F, bt.CHILD]: assert_bool(keys.has(variant)).override_failure_message( "BODY_TYPE_KEYS missing variant %d" % variant ).is_true() func test_body_type_key_average_m_is_average_m() -> void: var d := CharacterVisualDescriptor.new() d.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M assert_str(d.body_type_key()).is_equal("average_m") func test_body_type_key_child_is_child() -> void: var d := CharacterVisualDescriptor.new() d.body_type = CharacterVisualDescriptor.BodyType.CHILD assert_str(d.body_type_key()).is_equal("child") func test_body_type_keys_all_lowercase_snake_case() -> void: # File keys must match filesystem snake_case convention for key: String in CharacterVisualDescriptor.BODY_TYPE_KEYS.values(): assert_str(key).is_equal(key.to_lower()) # -- Wire format dictionaries ------------------------------------------------- func test_body_type_from_wire_covers_all_11_variants() -> void: var from_wire := CharacterVisualDescriptor.BODY_TYPE_FROM_WIRE assert_int(from_wire.size()).override_failure_message( "BODY_TYPE_FROM_WIRE must have 11 entries (one per BodyType variant)" ).is_equal(11) func test_body_type_to_wire_covers_all_11_variants() -> void: var to_wire := CharacterVisualDescriptor.BODY_TYPE_TO_WIRE assert_int(to_wire.size()).override_failure_message( "BODY_TYPE_TO_WIRE must have 11 entries" ).is_equal(11) func test_wire_round_trip_all_body_types() -> void: # from_wire → to_wire must be the identity for every variant var from_wire := CharacterVisualDescriptor.BODY_TYPE_FROM_WIRE var to_wire := CharacterVisualDescriptor.BODY_TYPE_TO_WIRE for wire_str: String in from_wire: var bt = from_wire[wire_str] assert_str(to_wire[bt]).override_failure_message( "to_wire(from_wire('%s')) should equal '%s'" % [wire_str, wire_str] ).is_equal(wire_str) # -- Default field values ----------------------------------------------------- func test_construction_does_not_crash() -> void: var d := CharacterVisualDescriptor.new() assert_bool(d != null).is_true() func test_default_body_type_is_average_m() -> void: var d := CharacterVisualDescriptor.new() assert_int(d.body_type).is_equal(CharacterVisualDescriptor.BodyType.AVERAGE_M) func test_default_head_id_is_empty_string() -> void: var d := CharacterVisualDescriptor.new() assert_str(d.head_id).is_equal("") func test_default_hair_id_is_empty_string() -> void: var d := CharacterVisualDescriptor.new() assert_str(d.hair_id).is_equal("") func test_default_hair_tint_is_white() -> void: var d := CharacterVisualDescriptor.new() assert_bool(d.hair_tint == Color.WHITE).override_failure_message( "Default hair_tint should be Color.WHITE" ).is_true() func test_default_facial_hair_id_is_empty_string() -> void: var d := CharacterVisualDescriptor.new() assert_str(d.facial_hair_id).is_equal("") func test_default_eyebrow_id_is_empty_string() -> void: var d := CharacterVisualDescriptor.new() assert_str(d.eyebrow_id).is_equal("") func test_default_eyebrow_tint_is_white() -> void: var d := CharacterVisualDescriptor.new() assert_bool(d.eyebrow_tint == Color.WHITE).is_true() func test_default_skin_tone_is_zero() -> void: var d := CharacterVisualDescriptor.new() assert_int(d.skin_tone).is_equal(0) func test_default_dicts_are_empty() -> void: var d := CharacterVisualDescriptor.new() assert_int(d.clothing_slots.size()).is_equal(0) assert_int(d.clothing_tints.size()).is_equal(0) assert_int(d.accessory_slots.size()).is_equal(0) assert_int(d.accessory_tints.size()).is_equal(0) # -- Mutation round-trips ----------------------------------------------------- func test_body_type_can_be_set_to_all_variants() -> void: var d := CharacterVisualDescriptor.new() var bt := CharacterVisualDescriptor.BodyType for variant in [bt.THIN_M, bt.THIN_F, bt.AVERAGE_M, bt.AVERAGE_F, bt.MUSCULAR_M, bt.MUSCULAR_F, bt.TEEN_M, bt.TEEN_F, bt.HEAVY_M, bt.HEAVY_F, bt.CHILD]: d.body_type = variant assert_int(d.body_type).is_equal(variant) func test_head_id_round_trips() -> void: var d := CharacterVisualDescriptor.new() d.head_id = "head_042" assert_str(d.head_id).is_equal("head_042") func test_hair_id_round_trips() -> void: var d := CharacterVisualDescriptor.new() d.hair_id = "ponytail" assert_str(d.hair_id).is_equal("ponytail") func test_skin_tone_accepts_full_range_0_to_8() -> void: # 9-tone palette: valid indices 0–8 var d := CharacterVisualDescriptor.new() for i in range(9): d.skin_tone = i assert_int(d.skin_tone).is_equal(i) func test_clothing_slots_can_be_populated() -> void: var d := CharacterVisualDescriptor.new() d.clothing_slots["torso"] = "coveralls_basic" d.clothing_slots["legs"] = "trousers_01" assert_int(d.clothing_slots.size()).is_equal(2) assert_str(d.clothing_slots["torso"]).is_equal("coveralls_basic") func test_accessory_slots_can_be_populated() -> void: var d := CharacterVisualDescriptor.new() d.accessory_slots["hat"] = "hat_hardhat" assert_str(d.accessory_slots["hat"]).is_equal("hat_hardhat") # -- from_dict() / to_dict() round-trips -------------------------------------- func test_from_dict_minimal_valid_input() -> void: # Only required fields; optional fields fall back to defaults var data := { "body_type": "AverageF", "head_id": "head_007", } var d := CharacterVisualDescriptor.from_dict(data) assert_bool(d != null).override_failure_message( "from_dict must succeed with required fields present" ).is_true() assert_int(d.body_type).is_equal(CharacterVisualDescriptor.BodyType.AVERAGE_F) assert_str(d.head_id).is_equal("head_007") func test_from_dict_all_fields() -> void: var data := { "body_type": "MuscularM", "head_id": "head_003", "hair_id": "buzz", "hair_tint": [0.2, 0.15, 0.1, 1.0], "facial_hair_id": "stubble", "facial_hair_tint": [0.1, 0.08, 0.06, 1.0], "eyebrow_id": "thick", "eyebrow_tint": [0.1, 0.08, 0.06, 1.0], "skin_tone": 4, "clothing_slots": {"torso": "shirt_02"}, "clothing_tints": {}, "accessory_slots": {}, "accessory_tints": {}, } var d := CharacterVisualDescriptor.from_dict(data) assert_bool(d != null).is_true() assert_int(d.body_type).is_equal(CharacterVisualDescriptor.BodyType.MUSCULAR_M) assert_str(d.hair_id).is_equal("buzz") assert_str(d.facial_hair_id).is_equal("stubble") assert_int(d.skin_tone).is_equal(4) assert_str(d.clothing_slots.get("torso", "")).is_equal("shirt_02") func test_from_dict_missing_body_type_returns_null() -> void: # Required field missing — must return null (not crash) var data := {"head_id": "head_001"} var d := CharacterVisualDescriptor.from_dict(data) assert_bool(d == null).override_failure_message( "from_dict must return null when body_type is absent" ).is_true() func test_from_dict_missing_head_id_returns_null() -> void: var data := {"body_type": "AverageM"} var d := CharacterVisualDescriptor.from_dict(data) assert_bool(d == null).override_failure_message( "from_dict must return null when head_id is absent" ).is_true() func test_from_dict_unknown_body_type_returns_null() -> void: # Unknown wire string — must return null, not crash or silently corrupt var data := {"body_type": "INVALID_TYPE", "head_id": "head_001"} var d := CharacterVisualDescriptor.from_dict(data) assert_bool(d == null).override_failure_message( "from_dict must return null for unknown body_type wire string" ).is_true() func test_to_dict_produces_string_body_type() -> void: # Wire format: body_type must be a string, not an integer var d := CharacterVisualDescriptor.new() d.body_type = CharacterVisualDescriptor.BodyType.THIN_F var wire := d.to_dict() assert_bool(wire["body_type"] is String).override_failure_message( "to_dict body_type must be a wire string, not an enum int" ).is_true() assert_str(wire["body_type"]).is_equal("ThinF") func test_full_round_trip_to_dict_from_dict() -> void: # Build a descriptor, encode, decode — all fields must survive var orig := CharacterVisualDescriptor.new() orig.body_type = CharacterVisualDescriptor.BodyType.HEAVY_F orig.head_id = "head_099" orig.hair_id = "long" orig.hair_tint = Color(0.5, 0.3, 0.1, 1.0) orig.facial_hair_id = "" orig.eyebrow_id = "thin" orig.skin_tone = 7 orig.clothing_slots = {"torso": "jacket_01"} var wire := orig.to_dict() var restored := CharacterVisualDescriptor.from_dict(wire) assert_bool(restored != null).is_true() assert_int(restored.body_type).is_equal(CharacterVisualDescriptor.BodyType.HEAVY_F) assert_str(restored.head_id).is_equal("head_099") assert_str(restored.hair_id).is_equal("long") assert_int(restored.skin_tone).is_equal(7) assert_str(restored.clothing_slots.get("torso", "")).is_equal("jacket_01") func test_to_dict_hair_tint_encodes_as_float_array() -> void: # Color must encode as [r, g, b, a] float array for rmp_serde var d := CharacterVisualDescriptor.new() d.hair_tint = Color(0.8, 0.3, 0.1, 1.0) var wire := d.to_dict() var tint = wire["hair_tint"] assert_bool(tint is Array).override_failure_message( "hair_tint in wire must be Array[float], not Color" ).is_true() assert_int((tint as Array).size()).is_equal(4) # -- Edge cases --------------------------------------------------------------- func test_separate_instances_have_independent_dicts() -> void: # Dictionary defaults must not be shared between instances (aliasing bug) var a := CharacterVisualDescriptor.new() var b := CharacterVisualDescriptor.new() a.clothing_slots["torso"] = "shirt_a" assert_bool("torso" in b.clothing_slots).override_failure_message( "clothing_slots must be independent across instances — not shared" ).is_false() func test_skin_tone_boundary_min_zero() -> void: var d := CharacterVisualDescriptor.new() d.skin_tone = 0 assert_int(d.skin_tone).is_equal(0) func test_skin_tone_boundary_max_eight() -> void: # 9-tone palette: index 8 is the maximum (very_deep_cool or very_deep_warm) var d := CharacterVisualDescriptor.new() d.skin_tone = 8 assert_int(d.skin_tone).is_equal(8) func test_facial_hair_empty_string_means_none() -> void: # Spec: "empty string = none" for facial_hair_id var d := CharacterVisualDescriptor.new() d.facial_hair_id = "" assert_str(d.facial_hair_id).is_equal("") func test_hardcoded_descriptor_matches_ticket_acceptance_criterion() -> void: # Ticket #703: "A hardcoded test descriptor can be constructed in GDScript # and matched to a Rust struct without panics." # This validates the GDScript half. Rust half lives in server-team tests. var d := CharacterVisualDescriptor.new() d.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_F d.head_id = "head_001" d.hair_id = "bob" d.hair_tint = Color(0.4, 0.25, 0.1, 1.0) d.facial_hair_id = "" d.eyebrow_id = "thin" d.eyebrow_tint = Color(0.3, 0.2, 0.1, 1.0) d.skin_tone = 2 d.clothing_slots = {"torso": "coveralls_basic", "legs": "trousers_basic"} d.clothing_tints = {"coveralls_basic": [Color.DARK_SLATE_GRAY]} d.accessory_slots = {} d.accessory_tints = {} assert_int(d.body_type).is_equal(CharacterVisualDescriptor.BodyType.AVERAGE_F) assert_str(d.head_id).is_equal("head_001") assert_str(d.hair_id).is_equal("bob") assert_int(d.skin_tone).is_equal(2) assert_int(d.clothing_slots.size()).is_equal(2) assert_int(d.accessory_slots.size()).is_equal(0) # And it must encode to a valid wire dict without crashing var wire := d.to_dict() assert_bool(wire.has("body_type")).is_true() assert_str(wire["body_type"]).is_equal("AverageF")