123 lines
5.4 KiB
Python
123 lines
5.4 KiB
Python
"""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."
|
|
)
|