fix(project): Reload personas and validate active AI settings on project switch
When switching projects, the previous project's project-specific persona and presets remained selected in the AI Settings panel because: 1. self.personas was not reloaded after switching project root 2. self.ui_active_persona / tool_preset / bias_profile / project_preset_name were not validated against the newly-loaded personas/presets Fix: - Reload self.personas from self.persona_manager in _refresh_from_project - Validate each active selection and reset to None/empty if it does not exist in the newly-loaded manager dictionaries - Push the active tool preset and bias profile to ai_client after the swap - Initialize self.ui_active_bias_profile in class attribute block (was only set later in __init__, causing AttributeError on direct attribute access) Tests: 4 new tests in tests/test_project_switch_persona_preset.py verify the reset behavior for persona, preset, tool preset, and global preset preservation.
This commit is contained in:
@@ -951,6 +951,7 @@ class AppController:
|
||||
self.prior_mma_dashboard_state = {}
|
||||
self.diagnostic_log: List[Dict[str, Any]] = []
|
||||
self.ui_active_tool_preset: str | None = None
|
||||
self.ui_active_bias_profile: str | None = None
|
||||
self.available_models: List[str] = []
|
||||
self.all_available_models: Dict[str, List[str]] = {}
|
||||
self.proposed_tracks: List[Dict[str, Any]] = []
|
||||
@@ -2782,6 +2783,17 @@ class AppController:
|
||||
self.tool_preset_manager.project_root = Path(self.active_project_root)
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
self.personas = self.persona_manager.load_all()
|
||||
if self.ui_active_persona and self.ui_active_persona not in self.personas:
|
||||
self.ui_active_persona = ""
|
||||
if self.ui_active_tool_preset and self.ui_active_tool_preset not in self.tool_presets:
|
||||
self.ui_active_tool_preset = None
|
||||
if self.ui_active_bias_profile and self.ui_active_bias_profile not in self.bias_profiles:
|
||||
self.ui_active_bias_profile = None
|
||||
if self.ui_project_preset_name and self.ui_project_preset_name not in self.presets:
|
||||
self.ui_project_preset_name = None
|
||||
ai_client.set_tool_preset(self.ui_active_tool_preset)
|
||||
ai_client.set_bias_profile(self.ui_active_bias_profile)
|
||||
raw_presets = proj.get("view_presets", [])
|
||||
if isinstance(raw_presets, dict):
|
||||
self.view_presets = [models.NamedViewPreset.from_dict({"name": name, **data}) for name, data in raw_presets.items()]
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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
|
||||
Reference in New Issue
Block a user