From f528b9461d71c395a1c362652463b3b2352f3cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 08:49:35 +0000 Subject: [PATCH 1/3] fix: prevent GTK crash by guarding _load_customizations during on_remove Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/d5a320b9-d9b8-4bd3-81c7-69a212a7a872 Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- .../customization_core/customization_core.py | 19 +++++ .../test_customization_core.py | 69 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/actions/cores/customization_core/customization_core.py b/actions/cores/customization_core/customization_core.py index 9219bfe..4203c54 100644 --- a/actions/cores/customization_core/customization_core.py +++ b/actions/cores/customization_core/customization_core.py @@ -19,6 +19,7 @@ class CustomizationCore(BaseCore): def __init__(self, window_implementation, customization_implementation, row_implementation, *args, **kwargs): # Must be set before create_ui_elements in BaseCore is called self.customization_expander = None + self._clearing = False super().__init__(*args, **kwargs) self.window_implementation = window_implementation self.customization_implementation = customization_implementation @@ -132,7 +133,25 @@ def _get_attributes(self) -> list[str]: attributes.extend(list(ha_entity.get(customization_const.ATTRIBUTES, {}).keys())) return attributes + @requires_initialization + def on_remove(self) -> None: + """Clean up after action was removed. + + Prevents _load_customizations() from modifying the customization + expander while it is being detached from the UI, which would corrupt + GTK's internal widget-tree state and cause a crash on the next + navigation back to this page. + """ + self._clearing = True + try: + super().on_remove() + finally: + self.customization_expander.clear_rows() + self._clearing = False + def _load_customizations(self) -> None: + if self._clearing: + return self.customization_expander.clear_rows() attributes = self._get_attributes() state = self.plugin_base.backend.get_entity(self.settings.get_entity()) diff --git a/test/actions/cores/customization_core/test_customization_core.py b/test/actions/cores/customization_core/test_customization_core.py index 0969da8..ca61e2f 100644 --- a/test/actions/cores/customization_core/test_customization_core.py +++ b/test/actions/cores/customization_core/test_customization_core.py @@ -348,3 +348,72 @@ def test_load_customizations(self): delete_connect_mock.assert_has_calls([call(base_const.CONNECT_CLICKED, instance._on_delete_customization, 0), call(base_const.CONNECT_CLICKED, instance._on_delete_customization, 1)]) up_connect_mock.assert_has_calls([call(base_const.CONNECT_CLICKED, instance._on_move_up, 0), call(base_const.CONNECT_CLICKED, instance._on_move_up, 1)]) down_connect_mock.assert_has_calls([call(base_const.CONNECT_CLICKED, instance._on_move_down, 0), call(base_const.CONNECT_CLICKED, instance._on_move_down, 1)]) + + def test_load_customizations_skipped_while_clearing(self): + instance = CustomizationCore.__new__(CustomizationCore) + instance.customization_expander = Mock() + instance._clearing = True + + instance._load_customizations() + + instance.customization_expander.clear_rows.assert_not_called() + + @patch('HomeAssistantPlugin.actions.cores.customization_core.customization_core.BaseCore.on_remove') + def test_on_remove_clears_expander_and_resets_flag(self, super_on_remove_mock): + instance = CustomizationCore.__new__(CustomizationCore) + instance.initialized = True + instance._clearing = False + instance.customization_expander = Mock() + + instance.on_remove() + + super_on_remove_mock.assert_called_once() + instance.customization_expander.clear_rows.assert_called_once() + self.assertFalse(instance._clearing) + + @patch('HomeAssistantPlugin.actions.cores.customization_core.customization_core.BaseCore.on_remove') + def test_on_remove_sets_clearing_flag_during_super_call(self, super_on_remove_mock): + """_clearing must be True when super().on_remove() runs so that any + refresh() call triggered by the base class skips _load_customizations().""" + instance = CustomizationCore.__new__(CustomizationCore) + instance.initialized = True + instance._clearing = False + instance.customization_expander = Mock() + + flag_during_super = [] + + def capture_flag(): + flag_during_super.append(instance._clearing) + + super_on_remove_mock.side_effect = capture_flag + + instance.on_remove() + + self.assertTrue(flag_during_super[0]) + self.assertFalse(instance._clearing) + + @patch('HomeAssistantPlugin.actions.cores.customization_core.customization_core.BaseCore.on_remove') + def test_on_remove_clears_expander_even_when_super_raises(self, super_on_remove_mock): + instance = CustomizationCore.__new__(CustomizationCore) + instance.initialized = True + instance._clearing = False + instance.customization_expander = Mock() + super_on_remove_mock.side_effect = RuntimeError("boom") + + with self.assertRaises(RuntimeError): + instance.on_remove() + + instance.customization_expander.clear_rows.assert_called_once() + self.assertFalse(instance._clearing) + + def test_on_remove_not_initialized(self): + instance = CustomizationCore.__new__(CustomizationCore) + instance.initialized = False + instance._clearing = False + instance.customization_expander = Mock() + + result = instance.on_remove() + + self.assertIsNone(result) + instance.customization_expander.clear_rows.assert_not_called() + self.assertFalse(instance._clearing) From f72146a4d6bf60097e34c72eba1a77858f2a5322 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 08:50:31 +0000 Subject: [PATCH 2/3] refactor: add inline comment for _clearing flag Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/d5a320b9-d9b8-4bd3-81c7-69a212a7a872 Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- actions/cores/customization_core/customization_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/cores/customization_core/customization_core.py b/actions/cores/customization_core/customization_core.py index 4203c54..cbc6337 100644 --- a/actions/cores/customization_core/customization_core.py +++ b/actions/cores/customization_core/customization_core.py @@ -19,6 +19,8 @@ class CustomizationCore(BaseCore): def __init__(self, window_implementation, customization_implementation, row_implementation, *args, **kwargs): # Must be set before create_ui_elements in BaseCore is called self.customization_expander = None + # Flag set during on_remove() to prevent _load_customizations() from + # adding child widgets to the expander while it is detached from the UI. self._clearing = False super().__init__(*args, **kwargs) self.window_implementation = window_implementation From d5025e8dbbdfe813597a47242abe3f2bc235e8dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:01:32 +0000 Subject: [PATCH 3/3] fix: set _clearing=False in test_load_customizations to match __new__ bypass Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/8832f1bc-a005-4429-a702-289d489821e7 Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- test/actions/cores/customization_core/test_customization_core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/actions/cores/customization_core/test_customization_core.py b/test/actions/cores/customization_core/test_customization_core.py index ca61e2f..4c86590 100644 --- a/test/actions/cores/customization_core/test_customization_core.py +++ b/test/actions/cores/customization_core/test_customization_core.py @@ -320,6 +320,7 @@ def test_get_attributes(self): def test_load_customizations(self): instance = CustomizationCore.__new__(CustomizationCore) instance.customization_expander = Mock() + instance._clearing = False instance._get_attributes = Mock() instance._get_attributes.return_value = ["attr1", "attr2"] instance.plugin_base = Mock()