"""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."
 )
