Files
basicmachines-co-basic-memory/test-int/mcp/test_project_state_sync_integration.py
Paul Hernandez 35e4f73ae8 fix: resolve project state inconsistency between MCP and CLI (#149)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-06-19 21:24:51 -05:00

174 lines
7.3 KiB
Python

"""Integration test for project state synchronization between MCP session and CLI config.
This test validates the fix for GitHub issue #148 where MCP session and CLI commands
had inconsistent project state, causing "Project not found" errors and edit failures.
The test simulates the exact workflow reported in the issue:
1. MCP server starts with a default project
2. Default project is changed via CLI/API
3. MCP tools should immediately use the new project (no restart needed)
4. All operations should work consistently in the new project context
"""
import pytest
from fastmcp import Client
@pytest.mark.asyncio
async def test_project_state_sync_after_default_change(mcp_server, app, config_manager):
"""Test that MCP session stays in sync when default project is changed."""
async with Client(mcp_server) as client:
# Step 1: Verify initial state - MCP should show test-project as current
initial_result = await client.call_tool("get_current_project", {})
assert len(initial_result) == 1
assert "Current project: test-project" in initial_result[0].text
# Step 2: Create a second project that we can switch to
create_result = await client.call_tool(
"create_memory_project",
{
"project_name": "minerva",
"project_path": "/tmp/minerva-test-project",
"set_default": False, # Don't set as default yet
},
)
assert len(create_result) == 1
assert "" in create_result[0].text
assert "minerva" in create_result[0].text
# Step 3: Change default project to minerva via set_default_project tool
# This simulates the CLI command `basic-memory project default minerva`
set_default_result = await client.call_tool(
"set_default_project", {"project_name": "minerva"}
)
assert len(set_default_result) == 1
assert "" in set_default_result[0].text
assert "minerva" in set_default_result[0].text
# Step 4: Verify MCP session immediately reflects the change (no restart needed)
# This tests the fix - session.refresh_from_config() should have been called
updated_result = await client.call_tool("get_current_project", {})
assert len(updated_result) == 1
# The fix should ensure these are consistent now:
updated_text = updated_result[0].text
assert "Current project: minerva" in updated_text
# Step 5: Verify config manager also shows the new default
assert config_manager.default_project == "minerva"
# Step 6: Test that note operations work in the new project context
# This validates that the identifier resolution works correctly
write_result = await client.call_tool(
"write_note",
{
"title": "Test Consistency Note",
"folder": "test",
"content": "# Test Note\n\nThis note tests project state consistency.\n\n- [test] Project state sync working",
"tags": "test,consistency",
},
)
assert len(write_result) == 1
assert "Test Consistency Note" in write_result[0].text
# Step 7: Test that we can read the note we just created
read_result = await client.call_tool("read_note", {"identifier": "Test Consistency Note"})
assert len(read_result) == 1
assert "Test Consistency Note" in read_result[0].text
assert "project state sync working" in read_result[0].text.lower()
# Step 8: Test that edit operations work (this was failing in the original issue)
edit_result = await client.call_tool(
"edit_note",
{
"identifier": "Test Consistency Note",
"operation": "append",
"content": "\n\n## Update\n\nEdit operation successful after project switch!",
},
)
assert len(edit_result) == 1
assert "added" in edit_result[0].text.lower() and "lines" in edit_result[0].text.lower()
# Step 9: Verify the edit was applied
final_read_result = await client.call_tool(
"read_note", {"identifier": "Test Consistency Note"}
)
assert len(final_read_result) == 1
final_content = final_read_result[0].text
assert "Edit operation successful" in final_content
# Clean up - switch back to test-project
await client.call_tool("switch_project", {"project_name": "test-project"})
@pytest.mark.asyncio
async def test_multiple_project_switches_maintain_consistency(mcp_server, app, config_manager):
"""Test that multiple project switches maintain consistent state."""
async with Client(mcp_server) as client:
# Create multiple test projects
for project_name in ["project-a", "project-b", "project-c"]:
await client.call_tool(
"create_memory_project",
{
"project_name": project_name,
"project_path": f"/tmp/{project_name}",
"set_default": False,
},
)
# Test switching between projects multiple times
for project_name in ["project-a", "project-b", "project-c", "test-project"]:
# Set as default
set_result = await client.call_tool(
"set_default_project", {"project_name": project_name}
)
assert "" in set_result[0].text
# Verify MCP session immediately reflects the change
current_result = await client.call_tool("get_current_project", {})
assert f"Current project: {project_name}" in current_result[0].text
# Verify config is also updated
assert config_manager.default_project == project_name
# Test that operations work in this project
note_title = f"Note in {project_name}"
write_result = await client.call_tool(
"write_note",
{
"title": note_title,
"folder": "test",
"content": f"# {note_title}\n\nTesting operations in {project_name}.",
"tags": "test",
},
)
assert note_title in write_result[0].text
# Clean up - switch back to test-project
await client.call_tool("set_default_project", {"project_name": "test-project"})
@pytest.mark.asyncio
async def test_session_handles_nonexistent_project_gracefully(mcp_server, app):
"""Test that session handles attempts to switch to nonexistent projects gracefully."""
async with Client(mcp_server) as client:
# Try to switch to a project that doesn't exist
switch_result = await client.call_tool(
"switch_project", {"project_name": "nonexistent-project"}
)
assert len(switch_result) == 1
result_text = switch_result[0].text
# Should show an error message
assert "Error:" in result_text
assert "not found" in result_text.lower()
assert "Available projects:" in result_text
assert "test-project" in result_text # Should list available projects
# Verify the session stays on the original project
current_result = await client.call_tool("get_current_project", {})
assert "Current project: test-project" in current_result[0].text