@@ -1503,6 +1503,37 @@ def test_ble_advertisement_restores_online_after_outage(self, sample_device):
15031503
15041504 assert coord ._states ["AA:BB:CC:DD:EE:FF:00:11" ].online is True
15051505
1506+ def test_ble_recovery_replaces_state_object_not_mutates (self , sample_device ):
1507+ """Regression for S3-007 (audit H2).
1508+
1509+ The recovery path must produce a *new* GoveeDeviceState instance via
1510+ ``dataclasses.replace`` and reassign into ``_states[matched_id]``.
1511+ In-place mutation of the existing instance causes
1512+ ``async_set_updated_data`` to pass the same dict-of-same-objects to
1513+ listeners, which can mask the change. This test pins the replace
1514+ semantic by asserting the dict slot now points at a different object.
1515+ """
1516+ from custom_components .govee .models import GoveeDeviceState
1517+
1518+ coord = self ._make_coordinator_with_devices (
1519+ {"AA:BB:CC:DD:EE:FF:00:11" : sample_device }
1520+ )
1521+ offline_state = GoveeDeviceState .create_empty ("AA:BB:CC:DD:EE:FF:00:11" )
1522+ offline_state .online = False
1523+ coord ._states ["AA:BB:CC:DD:EE:FF:00:11" ] = offline_state
1524+ original_id = id (offline_state )
1525+
1526+ info = self ._make_service_info ("Govee_H6072_754B" , "AA:BB:CC:DD:EE:FF" )
1527+ coord ._handle_ble_advertisement (info )
1528+
1529+ new_state = coord ._states ["AA:BB:CC:DD:EE:FF:00:11" ]
1530+ # Different object identity — proves dataclasses.replace was used.
1531+ assert id (new_state ) != original_id
1532+ # The original object was NOT mutated to True.
1533+ assert offline_state .online is False
1534+ # The replacement carries online=True.
1535+ assert new_state .online is True
1536+
15061537
15071538class TestTryBleCommand :
15081539 """Test the _try_ble_command method."""
0 commit comments