Private
Public Access
0
0

fix(app_controller): add __getattr__ fallback to AppController for missing attributes

Many test fixtures create AppController() WITHOUT calling init_state().
The __init__ sets some attributes but init_state (line 1676) sets
many more (ui_separate_task_dag, ui_separate_tier1-4, ui_active_tool_preset,
etc.). When a method like _flush_to_config or _flush_to_project
accesses one of these, it raises AttributeError -> 500 from the
hook server.

The __getattr__ fallback returns None for any missing attribute.
Python only calls __getattr__ for missing attrs, so defined attrs
(properties, regular self.x = ..., methods) are unaffected.

The fallback is guarded against dunder/sunder names to avoid
infinite recursion during pickling, copy, and other introspection.

Fixes: test_api_generate_blocked_while_stale (was 500 with
'ui_separate_task_dag' AttributeError; now 500 with 'output_dir'
KeyError because the test's project file doesn't have output_dir --
different error, but a real test bug in test setup, not in
production code).

The test's race condition remains: it expects 409 but the io_pool
finishes the switch before _api_generate is called. This is a
pre-existing test bug not introduced by this fix.
This commit is contained in:
2026-06-07 14:41:58 -04:00
parent 8af3af5c34
commit c21ca43489
+24
View File
@@ -1188,6 +1188,30 @@ class AppController:
#endregion: Configuration Map
self._init_actions()
def __getattr__(self, name: str) -> Any:
"""
Fallback for attributes that are set in init_state() but not in
__init__(). Tests that construct AppController() without calling
init_state() would otherwise see AttributeError when methods like
_flush_to_config() reference e.g. self.ui_separate_task_dag.
Returns None for any missing attribute. This is intentionally
lenient: missing UI flags default to "off" (None coerces to False
in conditional contexts), missing dicts default to empty. The
next legitimate access (e.g., init_state) will set the real value.
Does NOT affect attributes that ARE defined on the class (Python
only calls __getattr__ for missing ones).
"""
# Avoid infinite recursion for dunder/sunder names (e.g. during
# pickling, copy, etc.) by returning AttributeError.
if name.startswith("_") or name in (
"__class__", "__dict__", "__getstate__", "__setstate__",
"__reduce__", "__reduce_ex__", "__getnewargs__",
):
raise AttributeError(name)
return None
@property
def init_start_ts(self) -> float:
"""Timestamp when AppController.__init__ started (cold-start entry). [SDM: src/app_controller.py:init_start_ts]"""