@@ -1010,3 +1010,88 @@ def test_clear_scene_clears_both_scene_types(self):
10101010 assert state .active_scene is None
10111011 assert state .active_scene_name is None
10121012 assert state .active_diy_scene is None
1013+
1014+
1015+ class TestStatePreservationAcrossApiPoll :
1016+ """Test that restore-target fields survive API poll cycles."""
1017+
1018+ def test_last_color_preserved_across_api_poll (self ):
1019+ """Test last_color is preserved when API returns a fresh state."""
1020+ existing = GoveeDeviceState .create_empty ("test_id" )
1021+ existing .color = RGBColor (255 , 0 , 0 )
1022+ existing .apply_optimistic_scene ("scene_1" , "Sunset" )
1023+ assert existing .last_color == RGBColor (255 , 0 , 0 )
1024+
1025+ # Simulate API poll returning a fresh state (no last_color)
1026+ new_state = GoveeDeviceState .create_empty ("test_id" )
1027+ new_state .power_state = True
1028+
1029+ # Mimic coordinator preservation logic
1030+ if existing .last_color is not None :
1031+ new_state .last_color = existing .last_color
1032+
1033+ assert new_state .last_color == RGBColor (255 , 0 , 0 )
1034+
1035+ def test_last_color_temp_preserved_across_api_poll (self ):
1036+ """Test last_color_temp_kelvin is preserved when API returns a fresh state."""
1037+ existing = GoveeDeviceState .create_empty ("test_id" )
1038+ existing .color_temp_kelvin = 4500
1039+ existing .apply_optimistic_scene ("scene_1" , "Sunset" )
1040+ assert existing .last_color_temp_kelvin == 4500
1041+
1042+ new_state = GoveeDeviceState .create_empty ("test_id" )
1043+ new_state .power_state = True
1044+
1045+ if existing .last_color_temp_kelvin is not None :
1046+ new_state .last_color_temp_kelvin = existing .last_color_temp_kelvin
1047+
1048+ assert new_state .last_color_temp_kelvin == 4500
1049+
1050+ def test_last_scene_preserved_across_api_poll (self ):
1051+ """Test last_scene_id and last_scene_name survive API poll."""
1052+ existing = GoveeDeviceState .create_empty ("test_id" )
1053+ existing .apply_optimistic_scene ("scene_42" , "Aurora" )
1054+ assert existing .last_scene_id == "scene_42"
1055+ assert existing .last_scene_name == "Aurora"
1056+
1057+ new_state = GoveeDeviceState .create_empty ("test_id" )
1058+
1059+ if existing .last_scene_id is not None :
1060+ new_state .last_scene_id = existing .last_scene_id
1061+ if existing .last_scene_name is not None :
1062+ new_state .last_scene_name = existing .last_scene_name
1063+
1064+ assert new_state .last_scene_id == "scene_42"
1065+ assert new_state .last_scene_name == "Aurora"
1066+
1067+ def test_full_flow_color_scene_poll_clear (self ):
1068+ """End-to-end: set red → scene → API poll (colorRgb=0) → clear → red resolved."""
1069+ # Step 1: User sets red
1070+ state = GoveeDeviceState .create_empty ("test_id" )
1071+ state .color = RGBColor (255 , 0 , 0 )
1072+ state .power_state = True
1073+
1074+ # Step 2: User activates scene — saves red as last_color
1075+ state .apply_optimistic_scene ("scene_1" , "Party" )
1076+ assert state .last_color == RGBColor (255 , 0 , 0 )
1077+ assert state .color is None
1078+
1079+ # Step 3: API poll returns fresh state with colorRgb=0 (scene running)
1080+ api_state = GoveeDeviceState .create_empty ("test_id" )
1081+ api_state .power_state = True
1082+ api_state .color = RGBColor (0 , 0 , 0 ) # API returns black during scene
1083+
1084+ # Coordinator preserves memory fields
1085+ if state .active_scene :
1086+ api_state .active_scene = state .active_scene
1087+ if state .active_scene_name :
1088+ api_state .active_scene_name = state .active_scene_name
1089+ if state .last_color is not None :
1090+ api_state .last_color = state .last_color
1091+
1092+ # Step 4: Resolve color for clear_scene — reject black, fall back to last_color
1093+ color = api_state .color or api_state .last_color
1094+ if color and color .as_packed_int == 0 :
1095+ color = api_state .last_color
1096+
1097+ assert color == RGBColor (255 , 0 , 0 )
0 commit comments