"""Regression tests for AppController.__getattr__ ui_* default bug. The bug: `AppController.__getattr__` returns `None` for any attribute starting with `ui_` (e.g. `ui_separate_task_dag`, `ui_approve_modal_preview`). This makes `hasattr(ctrl, 'ui_X')` return True for ANY ui_X attribute, even if the attribute was never set on the instance. Consequence: `App.__setattr__` checks `hasattr(self.controller, name)` to decide where to route assignments. With the controller's broken __getattr__, all `ui_*` assignments get routed to the controller. But the controller's `__getattr__` then returns None on read, silently dropping the assigned value. This breaks the `if not hasattr(app, 'foo'): app.foo = False` pattern in render_approve_script_modal (gui_2.py:4894): the hasattr returns True (via the controller), so the assignment never runs, and the next line tries to call checkbox with None. The fix: AppController.__getattr__ should only return None for attributes that have been set (in self.__dict__). For unset attributes, raise AttributeError so hasattr() returns False. """ import pytest import sys import os from typing import Any ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, ROOT) def test_hasattr_returns_false_for_unset_ui_attribute(mock_app: Any) -> None: """hasattr(ctrl, 'ui_test_attr_xyz') must return False when the attribute is not set on the instance. Otherwise the App.__setattr__ routing logic misbehaves. """ app = mock_app ctrl = app.controller # Use a unique attribute name guaranteed not to be in init_state() attr_name = "ui_test_attr_xyz_unique_12345" # Force the attribute lookup assert hasattr(ctrl, attr_name) is False, ( f"hasattr(ctrl, {attr_name!r}) returned True for an unset attribute. " "AppController.__getattr__ is returning None for any ui_* attribute, " "which breaks hasattr() and the App.__setattr__ routing logic." )