Private
Public Access
0
0

test(reset): regression for 3 pre-existing controller bugs

This commit is contained in:
2026-06-10 09:16:46 -04:00
parent 4284ec6eba
commit b96d709efb
+122
View File
@@ -0,0 +1,122 @@
"""Regression tests for 3 pre-existing bugs in AppController.
Bug 1: _handle_reset_session zeroes mma_tier_usage to empty dicts; the downstream
_flush_to_project crashes with KeyError: 'model'. (Commits fe240db4 introduced.)
Bug 2: __init__ does not set self.context_preset_manager; save_context_preset
and load_context_preset crash. (Lost in 72f8f466.)
Bug 3: __getattr__ returns None for 'persona_manager', making hasattr() return
True (the accompanying comment claims False, which is wrong).
The integration symptom of Bug 1 was test_context_sim_live polling ai_status
for 60s and seeing the constant 'switching to: temp_livecontextsim (stale ui -
ops disabled)' string (older runs) or 'error: \\'model\\'' (newer runs after
sim_context.py added an 'error in s' early-break check).
These tests exercise the exact code paths that were crashing, in isolation,
to prove the fixes prevent the original failures.
The tests do NOT require the live_gui fixture. They use a real AppController()
with a tmp_path for the project file, matching the pattern in
tests/test_handle_reset_session_clears_project.py.
"""
import pytest
import tomllib
from pathlib import Path
from src.app_controller import AppController
@pytest.fixture
def controller(tmp_path: Path) -> AppController:
"""Build a real AppController with a writable project file."""
proj_path = tmp_path / "test_project.toml"
proj_path.write_text("[project]\nname = 'TestProject'\n")
ctrl = AppController()
ctrl.active_project_path = str(proj_path)
# _flush_to_project reads several UI flags that __init__ does not set
# (ui_project_preset_name, ui_word_wrap, ui_gemini_cli_path,
# ui_auto_add_history). Set them so the test exercises the
# mma_tier_usage code path without tripping on unrelated missing attrs.
ctrl.ui_project_preset_name = None
ctrl.ui_word_wrap = True
ctrl.ui_gemini_cli_path = ""
ctrl.ui_auto_add_history = False
yield ctrl
def test_reset_session_makes_flush_to_project_not_crash(controller: AppController) -> None:
"""Bug 1 fix: After _handle_reset_session, _flush_to_project must not raise KeyError.
Pre-fix: the reset zeroes mma_tier_usage to empty dicts; _flush_to_project
crashes on d['model']. Post-fix: the reset pre-populates the dicts (matching
__init__ defaults), and _flush_to_project uses d.get('model') as a defensive
fallback. This test asserts the round-trip works.
"""
for tier in ("Tier 1", "Tier 2", "Tier 3", "Tier 4"):
assert "model" in controller.mma_tier_usage[tier], (
f"precondition failed: tier {tier} has no 'model' key in __init__"
)
controller._handle_reset_session()
for tier in ("Tier 1", "Tier 2", "Tier 3", "Tier 4"):
assert "model" in controller.mma_tier_usage[tier], (
f"_handle_reset_session stripped 'model' from {tier}: "
f"{controller.mma_tier_usage[tier]!r}"
)
assert "provider" in controller.mma_tier_usage[tier], (
f"_handle_reset_session stripped 'provider' from {tier}: "
f"{controller.mma_tier_usage[tier]!r}"
)
controller._flush_to_project()
assert Path(controller.active_project_path).exists()
def test_flush_to_project_is_defensive_against_partial_tier_dict(controller: AppController) -> None:
"""Bug 1 fix (defense in depth): _flush_to_project must not raise KeyError on partial dicts.
This is the defense-in-depth test for the d.get('model') change. Simulates
a code path (like _handle_mma_state_update at line 484-497) that replaces
the entire mma_tier_usage[tier] entry with a partial dict.
"""
controller.mma_tier_usage["Tier 3"] = {"input": 0, "output": 0, "provider": "gemini"}
controller._flush_to_project()
with open(controller.active_project_path, "rb") as f:
saved = tomllib.load(f)
tier_models = saved.get("mma", {}).get("tier_models", {})
assert "Tier 3" in tier_models, f"Tier 3 missing from saved tier_models: {tier_models!r}"
assert tier_models["Tier 3"].get("model") in (None, ""), (
f"Expected None or empty model for the partial-dict case, got "
f"{tier_models['Tier 3'].get('model')!r}"
)
def test_context_preset_manager_is_initialized(controller: AppController) -> None:
"""Bug 2 fix: self.context_preset_manager must be a ContextPresetManager, not None.
Pre-fix: __init__ did not set self.context_preset_manager; save_context_preset
and load_context_preset both crashed with AttributeError. Post-fix: __init__
sets it to ContextPresetManager() (the line was lost in 72f8f466 and re-added).
"""
assert controller.context_preset_manager is not None, (
f"context_preset_manager is None; the __init__ line is missing"
)
from src.context_presets import ContextPresetManager
assert isinstance(controller.context_preset_manager, ContextPresetManager), (
f"context_preset_manager is {type(controller.context_preset_manager).__name__}, "
f"expected ContextPresetManager"
)
def test_hasattr_persona_manager_returns_false_for_fresh_controller() -> None:
"""Bug 3 fix: hasattr(ctrl, 'persona_manager') must be False for a fresh AppController.
Pre-fix: __getattr__ returned None for 'persona_manager' (in _LAZY_MANAGER_DEFAULTS),
making hasattr() return True. The comment claimed hasattr() returns False but
that's wrong. Post-fix: 'persona_manager' is removed from _LAZY_MANAGER_DEFAULTS,
so __getattr__ raises AttributeError, so hasattr() returns False.
"""
ctrl = AppController()
assert not hasattr(ctrl, "persona_manager"), (
f"hasattr(ctrl, 'persona_manager') returned True for a fresh AppController. "
f"__getattr__ likely still returns None for it. Check _LAZY_MANAGER_DEFAULTS "
f"in src/app_controller.py."
)