7df65dff14
Two fixes for the regression introduced in b92daef3 (and an additional
hardening for the persona->context_preset stale-reference class of bug):
1. Regression: persona_manager was missing on first project load.
_load_active_project creates preset_manager and tool_preset_manager
but did not create persona_manager, so the new
self.personas = self.persona_manager.load_all() line in
_refresh_from_project raised AttributeError on app startup before
the post-_load_active_project persona_manager creation could run.
Fix: create self.persona_manager in _load_active_project alongside
the other managers, so the manager is available when
_refresh_from_project runs.
2. Stale reference: persona's context_preset field pointed to a
preset (e.g. 'GTE') that no longer exists in the project, causing
load_context_preset to raise KeyError and crash the persona
selector panel (which triggered the cascading 'Missing End()' imgui
assertion).
Fix: wrap the load_context_preset call in render_persona_selector_panel
with try/except KeyError, surface the error in app.ai_status, and
clear app.ui_active_context_preset to keep the GUI state consistent.
Tests: 2 new tests in tests/test_project_switch_persona_preset.py
- test_load_active_project_creates_persona_manager (regression guard)
- test_load_context_preset_missing_raises_keyerror (verifies the
contract that load_context_preset raises for missing names; the
GUI layer is now responsible for catching the error)
173 lines
6.6 KiB
Python
173 lines
6.6 KiB
Python
import os
|
|
import json
|
|
import pytest
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
from src.app_controller import AppController
|
|
from src import models
|
|
from src.personas import PersonaManager
|
|
from src import presets, tool_presets
|
|
from src import project_manager
|
|
|
|
|
|
def _setup_two_projects(tmp_path):
|
|
project_a_dir = tmp_path / "project_a"
|
|
project_a_dir.mkdir()
|
|
project_a_path = project_a_dir / "project.toml"
|
|
project_a_path.write_text('[project]\nname = "project_a"\nactive_preset = "PresetA"\n')
|
|
|
|
project_b_dir = tmp_path / "project_b"
|
|
project_b_dir.mkdir()
|
|
project_b_path = project_b_dir / "project.toml"
|
|
project_b_path.write_text('[project]\nname = "project_b"\n')
|
|
|
|
(project_a_dir / "project_personas.toml").write_text(
|
|
'[personas."PersonaA"]\nsystem_prompt = "Project A persona"\npreferred_models = []\n'
|
|
)
|
|
(project_b_dir / "project_personas.toml").write_text(
|
|
'[personas."PersonaB"]\nsystem_prompt = "Project B persona"\npreferred_models = []\n'
|
|
)
|
|
(project_a_dir / "project_presets.toml").write_text(
|
|
'[presets."PresetA"]\nsystem_prompt = "Project A preset"\n'
|
|
)
|
|
(project_b_dir / "project_presets.toml").write_text(
|
|
'[presets."PresetB"]\nsystem_prompt = "Project B preset"\n'
|
|
)
|
|
|
|
return project_a_path, project_b_path
|
|
|
|
|
|
def test_switch_project_resets_invalid_persona(tmp_path, monkeypatch):
|
|
project_a_path, project_b_path = _setup_two_projects(tmp_path)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl.project = project_manager.load_project(str(project_a_path))
|
|
ctrl.preset_manager = presets.PresetManager(Path(project_a_path).parent)
|
|
ctrl.tool_preset_manager = tool_presets.ToolPresetManager(Path(project_a_path).parent)
|
|
ctrl.persona_manager = PersonaManager(Path(project_a_path).parent)
|
|
ctrl._refresh_from_project()
|
|
|
|
assert "PersonaA" in ctrl.personas
|
|
assert "PersonaB" not in ctrl.personas
|
|
ctrl.ui_active_persona = "PersonaA"
|
|
|
|
ctrl._switch_project(str(project_b_path))
|
|
|
|
assert "PersonaA" not in ctrl.personas
|
|
assert "PersonaB" in ctrl.personas
|
|
assert ctrl.ui_active_persona == ""
|
|
|
|
|
|
def test_switch_project_resets_invalid_preset(tmp_path, monkeypatch):
|
|
project_a_path, project_b_path = _setup_two_projects(tmp_path)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl.project = project_manager.load_project(str(project_a_path))
|
|
ctrl.preset_manager = presets.PresetManager(Path(project_a_path).parent)
|
|
ctrl.tool_preset_manager = tool_presets.ToolPresetManager(Path(project_a_path).parent)
|
|
ctrl.persona_manager = PersonaManager(Path(project_a_path).parent)
|
|
ctrl._refresh_from_project()
|
|
|
|
assert ctrl.ui_project_preset_name == "PresetA"
|
|
|
|
ctrl._switch_project(str(project_b_path))
|
|
|
|
assert "PresetA" not in ctrl.presets
|
|
assert "PresetB" in ctrl.presets
|
|
assert ctrl.ui_project_preset_name is None
|
|
|
|
|
|
def test_switch_project_resets_invalid_tool_preset(tmp_path, monkeypatch):
|
|
project_a_path, project_b_path = _setup_two_projects(tmp_path)
|
|
|
|
(Path(project_a_path).parent / "project_tool_presets.toml").write_text(
|
|
'[presets."ToolA"]\ndescription = "A"\n[presets.tools."run_powershell"]\nweight = 1\n'
|
|
)
|
|
(Path(project_b_path).parent / "project_tool_presets.toml").write_text(
|
|
'[presets."ToolB"]\ndescription = "B"\n[presets.tools."run_powershell"]\nweight = 1\n'
|
|
)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl.project = project_manager.load_project(str(project_a_path))
|
|
ctrl.preset_manager = presets.PresetManager(Path(project_a_path).parent)
|
|
ctrl.tool_preset_manager = tool_presets.ToolPresetManager(Path(project_a_path).parent)
|
|
ctrl.persona_manager = PersonaManager(Path(project_a_path).parent)
|
|
ctrl._refresh_from_project()
|
|
|
|
ctrl.ui_active_tool_preset = "ToolA"
|
|
assert "ToolA" in ctrl.tool_presets
|
|
|
|
ctrl._switch_project(str(project_b_path))
|
|
|
|
assert "ToolA" not in ctrl.tool_presets
|
|
assert "ToolB" in ctrl.tool_presets
|
|
assert ctrl.ui_active_tool_preset is None
|
|
|
|
|
|
def test_switch_project_preserves_global_preset(tmp_path, monkeypatch):
|
|
project_a_path, project_b_path = _setup_two_projects(tmp_path)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl.project = project_manager.load_project(str(project_a_path))
|
|
ctrl.preset_manager = presets.PresetManager(Path(project_a_path).parent)
|
|
ctrl.tool_preset_manager = tool_presets.ToolPresetManager(Path(project_a_path).parent)
|
|
ctrl.persona_manager = PersonaManager(Path(project_a_path).parent)
|
|
ctrl._refresh_from_project()
|
|
|
|
ctrl.ui_global_preset_name = "GlobalPresetA"
|
|
original_global = ctrl.ui_global_preset_name
|
|
|
|
ctrl._switch_project(str(project_b_path))
|
|
|
|
assert ctrl.ui_global_preset_name == original_global
|
|
|
|
|
|
def test_load_active_project_creates_persona_manager(tmp_path, monkeypatch):
|
|
project_a_path, _ = _setup_two_projects(tmp_path)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_configure_mcp_for_project", lambda: None)
|
|
|
|
assert not hasattr(ctrl, "persona_manager")
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl._load_active_project()
|
|
|
|
assert hasattr(ctrl, "persona_manager")
|
|
assert "PersonaA" in ctrl.personas
|
|
assert ctrl.ui_active_bias_profile is None or ctrl.ui_active_bias_profile in ctrl.bias_profiles
|
|
|
|
|
|
def test_load_context_preset_missing_raises_keyerror(tmp_path, monkeypatch):
|
|
project_a_path, _ = _setup_two_projects(tmp_path)
|
|
|
|
ctrl = AppController()
|
|
monkeypatch.setattr(ctrl, "_rebuild_rag_index", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_flush_to_project", lambda: None)
|
|
monkeypatch.setattr(ctrl, "_configure_mcp_for_project", lambda: None)
|
|
|
|
ctrl.active_project_path = str(project_a_path)
|
|
ctrl._load_active_project()
|
|
|
|
with pytest.raises(KeyError, match="Context preset 'NonexistentPreset' not found"):
|
|
ctrl.load_context_preset("NonexistentPreset")
|