Private
Public Access
0
0
Files
manual_slop/tests/test_project_switch_persona_preset.py
T
ed 36f3292249 fix(project): Reload context_files from new project on project switch
When switching projects, the previous project's context_files remained
visible in the Context Composition panel because the controller's
self.context_files list was not reloaded from the new project's TOML
files.paths entry.

Fix in _refresh_from_project:
- After loading self.files from the project TOML, populate
  self.context_files with deep copies of those FileItem objects
- Reset self._app.ui_selected_context_files to match the new project's
  auto_aggregate set
- Guard the _app access with hasattr so the controller is usable
  standalone (in tests, headless mode, etc.) without an attached App

Test: 1 new test in tests/test_project_switch_persona_preset.py
- test_switch_project_resets_context_files: switches from project_a
  (forth + gte_hello files) to project_b (gencpp timing files) and
  asserts context_files contains ONLY project_b's files
2026-06-04 21:03:16 -04:00

221 lines
8.3 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")
def test_switch_project_resets_context_files(tmp_path, monkeypatch):
project_a_path, project_b_path = _setup_two_projects(tmp_path)
proj_a_data = '''[project]
name = "project_a"
[files]
base_dir = "."
paths = [
{ path = "C:/projects/forth/bootslop/main.c", view_mode = "full" },
{ path = "C:/projects/Pikuma/ps1/code/gte_hello/hello_gte.c", view_mode = "full" },
]
'''
project_a_path.write_text(proj_a_data)
proj_b_data = '''[project]
name = "project_b"
[files]
base_dir = "."
paths = [
{ path = "C:/projects/gencpp/base/dependencies/timing.cpp", view_mode = "full" },
{ path = "C:/projects/gencpp/base/dependencies/timing.hpp", view_mode = "full" },
]
'''
project_b_path.write_text(proj_b_data)
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 len(ctrl.context_files) == 2
assert any("forth" in f.path for f in ctrl.context_files)
assert any("gte_hello" in f.path for f in ctrl.context_files)
ctrl._switch_project(str(project_b_path))
assert len(ctrl.context_files) == 2
assert all("gencpp" in f.path for f in ctrl.context_files)
assert not any("forth" in f.path for f in ctrl.context_files)
assert not any("gte_hello" in f.path for f in ctrl.context_files)