1+ """Test project state consistency between MCP and CLI components."""
2+
3+ import asyncio
4+ import os
5+ import tempfile
6+ from pathlib import Path
7+ from typing import Generator
8+
9+ import pytest
10+
11+ from basic_memory import config
12+ from basic_memory .config import ConfigManager , BasicMemoryConfig
13+ from basic_memory .mcp .project_session import ProjectSession , session , get_active_project
14+ from basic_memory .cli .commands .project import list_projects , set_default_project
15+ from basic_memory .services .project_service import ProjectService
16+ from basic_memory .repository .project_repository import ProjectRepository
17+ from basic_memory .models .project import Project
18+
19+
20+ @pytest .fixture
21+ def temp_config_dir () -> Generator [Path , None , None ]:
22+ """Create a temporary config directory for testing."""
23+ with tempfile .TemporaryDirectory () as temp_dir :
24+ temp_path = Path (temp_dir )
25+ yield temp_path
26+
27+
28+ @pytest .fixture
29+ def temp_projects (temp_config_dir : Path ) -> dict [str , Path ]:
30+ """Create temporary project directories."""
31+ projects = {}
32+ for name in ["main" , "minerva" , "test-project" ]:
33+ project_path = temp_config_dir / name
34+ project_path .mkdir (parents = True , exist_ok = True )
35+ projects [name ] = project_path
36+ return projects
37+
38+
39+ @pytest .fixture
40+ def config_manager_with_projects (temp_config_dir : Path , temp_projects : dict [str , Path ]) -> ConfigManager :
41+ """Create a config manager with test projects."""
42+ # Mock the config directory
43+ original_config_dir = config .config_manager .config_dir
44+ original_config_file = config .config_manager .config_file
45+
46+ try :
47+ config .config_manager .config_dir = temp_config_dir
48+ config .config_manager .config_file = temp_config_dir / "config.json"
49+
50+ # Create test config
51+ test_config = BasicMemoryConfig (
52+ projects = {name : str (path ) for name , path in temp_projects .items ()},
53+ default_project = "main"
54+ )
55+ config .config_manager .config = test_config
56+ config .config_manager .save_config (test_config )
57+
58+ yield config .config_manager
59+ finally :
60+ # Restore original paths
61+ config .config_manager .config_dir = original_config_dir
62+ config .config_manager .config_file = original_config_file
63+
64+
65+ class TestProjectStateConsistency :
66+ """Test cases for project state consistency between MCP and CLI."""
67+
68+ def test_project_session_initialization (self , config_manager_with_projects : ConfigManager ):
69+ """Test that project session initializes with correct default project."""
70+ # Create a new session
71+ test_session = ProjectSession ()
72+
73+ # Initialize with default project from config
74+ default_project = config_manager_with_projects .default_project
75+ test_session .initialize (default_project )
76+
77+ assert test_session .get_current_project () == "main"
78+ assert test_session .get_default_project () == "main"
79+
80+ def test_mcp_cli_project_state_mismatch (self , config_manager_with_projects : ConfigManager ):
81+ """Test reproducing the MCP vs CLI project state mismatch."""
82+ # Initialize MCP session
83+ test_session = ProjectSession ()
84+ test_session .initialize ("main" )
85+
86+ # Simulate CLI setting default project to "minerva"
87+ config_manager_with_projects .set_default_project ("minerva" )
88+
89+ # MCP session should still show "main" because it hasn't been reloaded
90+ assert test_session .get_current_project () == "main"
91+
92+ # Config manager should show "minerva" as default
93+ assert config_manager_with_projects .default_project == "minerva"
94+
95+ # This demonstrates the inconsistency issue
96+
97+ def test_default_project_persistence (self , config_manager_with_projects : ConfigManager , temp_config_dir : Path ):
98+ """Test that default project setting persists correctly."""
99+ # Set default project
100+ config_manager_with_projects .set_default_project ("minerva" )
101+
102+ # Verify it's saved to config file
103+ config_file = temp_config_dir / "config.json"
104+ assert config_file .exists ()
105+
106+ # Create new config manager to simulate fresh load
107+ new_manager = ConfigManager ()
108+ new_manager .config_dir = temp_config_dir
109+ new_manager .config_file = config_file
110+ reloaded_config = new_manager .load_config ()
111+
112+ assert reloaded_config .default_project == "minerva"
113+
114+ async def test_project_not_found_after_default_change (self , config_manager_with_projects : ConfigManager ):
115+ """Test reproducing the 'Project not found' error after default change."""
116+ # Initialize with main project
117+ test_session = ProjectSession ()
118+ test_session .initialize ("main" )
119+
120+ # Change default in config
121+ config_manager_with_projects .set_default_project ("minerva" )
122+
123+ # Simulate the status command trying to access the project
124+ # This should fail because session still thinks current project is "main"
125+ # but config now says default is "minerva"
126+
127+ current_from_session = test_session .get_current_project () # Returns "main"
128+ current_from_config = config_manager_with_projects .default_project # Returns "minerva"
129+
130+ assert current_from_session != current_from_config
131+ # This mismatch is what causes the "Project not found" error
132+
133+ def test_edit_note_identifier_resolution_issue (self , config_manager_with_projects : ConfigManager ):
134+ """Test the edit note identifier resolution when project state is inconsistent."""
135+ # Initialize MCP session with one project
136+ test_session = ProjectSession ()
137+ test_session .initialize ("main" )
138+
139+ # Simulate project switch in config but not in session
140+ config_manager_with_projects .set_default_project ("minerva" )
141+
142+ # When edit_note tries to use get_active_project, it might get confused
143+ # about which project context to use
144+ try :
145+ # This would normally call get_active_project(None)
146+ # which should use session.get_current_project()
147+ active_project = get_active_project (None )
148+ # The project config returned might not match the actual session state
149+ assert active_project .name == "main" # Session state
150+
151+ # But config shows different default
152+ assert config_manager_with_projects .default_project == "minerva"
153+
154+ except Exception as e :
155+ # This might fail if project lookup is inconsistent
156+ pytest .fail (f"get_active_project failed with inconsistent state: { e } " )
157+
158+ def test_session_reload_fixes_inconsistency (self , config_manager_with_projects : ConfigManager ):
159+ """Test that reloading session fixes the inconsistency."""
160+ # Initialize session
161+ test_session = ProjectSession ()
162+ test_session .initialize ("main" )
163+
164+ # Change config
165+ config_manager_with_projects .set_default_project ("minerva" )
166+
167+ # Verify inconsistency
168+ assert test_session .get_current_project () == "main"
169+ assert config_manager_with_projects .default_project == "minerva"
170+
171+ # Reinitialize session with new default
172+ new_default = config_manager_with_projects .default_project
173+ test_session .initialize (new_default )
174+
175+ # Now they should match
176+ assert test_session .get_current_project () == "minerva"
177+ assert config_manager_with_projects .default_project == "minerva"
178+
179+ async def test_project_service_default_setting_propagation (
180+ self ,
181+ config_manager_with_projects : ConfigManager ,
182+ session_maker ,
183+ project_repository : ProjectRepository
184+ ):
185+ """Test that project service properly propagates default changes."""
186+ # Create projects in database to match config
187+ for name , path in config_manager_with_projects .projects .items ():
188+ project_data = {
189+ "name" : name ,
190+ "path" : str (path ),
191+ "permalink" : name .lower ().replace (" " , "-" ),
192+ "is_active" : True ,
193+ }
194+ await project_repository .create (project_data )
195+
196+ # Set main as default in database
197+ main_project = await project_repository .get_by_name ("main" )
198+ await project_repository .set_as_default (main_project .id )
199+
200+ # Initialize project service
201+ project_service = ProjectService (project_repository )
202+
203+ # Change default via service
204+ await project_service .set_default_project ("minerva" )
205+
206+ # Verify both config and database are updated
207+ assert config_manager_with_projects .default_project == "minerva"
208+
209+ db_default = await project_repository .get_default_project ()
210+ assert db_default is not None
211+ assert db_default .name == "minerva"
212+
213+ def test_cli_config_reload_requirement (self , config_manager_with_projects : ConfigManager ):
214+ """Test that CLI commands require config reload after default change."""
215+ # This test verifies the issue mentioned in the CLI command:
216+ # "Reload configuration to apply the change"
217+
218+ # Change default project
219+ original_default = config_manager_with_projects .default_project
220+ config_manager_with_projects .set_default_project ("minerva" )
221+
222+ # Without reload, global config might still show old value
223+ # This is what causes the inconsistency
224+ assert config_manager_with_projects .default_project == "minerva"
225+
226+ # But global config object might not be updated
227+ # (This would require actual module reload which we can't easily test)
228+
229+ # The fix should ensure config changes are immediately visible
230+ # to all components without requiring restart
0 commit comments