Private
Public Access
0
0
Files
manual_slop/tests/test_app_getattr_hasattr_bug.py
T
ed bcdc26d0bd fix(gui): correct __getattr__ to not silently return None for missing ui_ attrs
PR1 follow-up (the actual IM_ASSERT root cause fix).

The IM_ASSERT in 'MainDockSpace' was triggered by the
render_approve_script_modal function (gui_2.py:4895) calling
imgui.checkbox with a None value for app.ui_approve_modal_preview.

The chain of bugs:

1. AppController.__getattr__ returned None for ANY ui_ attribute
   (line 1237-1238). This was intended as a safety net for ui_*
   flags defined in __init__ but it was too généreux: it returned
   None for ui_ attrs that were NEVER set.

2. The pattern in render_approve_script_modal:
      if not hasattr(app, 'ui_approve_modal_preview'):
          app.ui_approve_modal_preview = False
      _, app.ui_approve_modal_preview = imgui.checkbox(..., app.ui_approve_modal_preview)
   relied on hasattr() returning False for unset attrs to trigger
   the initialization. But the App.__setattr__ checks
   hasattr(self.controller, name) to decide where to route
   assignments. The controller's __getattr__ returned None for
   ui_approve_modal_preview, so hasattr() returned True. The
   App.__setattr__ routed the assignment to the controller.
   The controller's __getattr__ then returned None on read,
   silently dropping the False value.

3. The next line called imgui.checkbox with None, which raised
   a TypeError. The TypeError propagated out of
   render_approve_script_modal without closing the modal,
   leaving the ImGui scope stack unbalanced. The unbalanced
   scope triggered IM_ASSERT(Missing End()) on the next frame.

Fix: AppController.__getattr__ now only returns None for an
EXPLICIT allowlist of ui_ attrs that are defined in __init__.
For any other missing attribute (including the case
'hasattr() should return False'), it raises AttributeError.

The App.__getattr__ was also fixed (per the test) to check
hasattr(controller, name) before delegating. This is defense in
depth in case other __getattr__ patterns are added.

Test verification (TDD red → green):
- 1/1 test_app_getattr_hasattr_bug PASSES (verifies hasattr
  returns False for unset attrs via App.__getattr__)
- 1/1 test_app_controller_getattr_ui_bug PASSES (verifies hasattr
  returns False for unset ui_ attrs on controller)

Live verification:
- 4 sims + test_live_workflow + 2 markdown tests: 7/7 PASS in 83.15s
- Previously failed at 200s+ with 'cannot schedule new futures after
  shutdown' / 121s with 'GUI is degraded before test starts'
- Now passes cleanly. The IM_ASSERT no longer fires.

13/13 related unit tests pass (app_controller_* + app_run_* +
app_getattr_*). No regressions in 51/51 io_pool/warmup/sigint/etc.
unit tests.
2026-06-08 23:45:25 -04:00

41 lines
1.6 KiB
Python

"""Regression tests for the App.__getattr__ passthrough bug.
The bug: `App.__getattr__` delegates unknown attributes to the
controller via `getattr(self.controller, name)`. If the controller
doesn't have the attribute either, `getattr` returns None (the default).
This breaks `hasattr(app, 'unknown_attr')`:
- hasattr() checks the instance dict first
- If not found, it calls __getattr__
- __getattr__ returns None instead of raising AttributeError
- hasattr() sees a non-exception result, returns True
- Caller's `if not hasattr(...)` is False, never initializes the attr
The fix: __getattr__ should raise AttributeError for attributes that
the controller also doesn't have, so hasattr() works correctly.
"""
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_attribute(mock_app: Any) -> None:
"""hasattr(app, 'definitely_unique_test_attr_xyz123') must return False
when neither App nor controller has the attribute. This is the exact
pattern that fails in render_approve_script_modal (gui_2.py:4894):
`if not hasattr(app, 'foo'): app.foo = False`.
"""
app = mock_app
# Use a unique attribute name guaranteed not to exist
attr_name = "definitely_unique_test_attr_xyz123"
# Assert hasattr returns False
assert hasattr(app, attr_name) is False, (
f"hasattr returned True for {attr_name!r} which is not set on App "
"or controller. This breaks the `if not hasattr: initialize` "
"pattern in render_approve_script_modal (gui_2.py:4894)."
)