|
| 1 | +"""Integration test for project state synchronization between MCP session and CLI config. |
| 2 | +
|
| 3 | +This test validates the fix for GitHub issue #148 where MCP session and CLI commands |
| 4 | +had inconsistent project state, causing "Project not found" errors and edit failures. |
| 5 | +
|
| 6 | +The test simulates the exact workflow reported in the issue: |
| 7 | +1. MCP server starts with a default project |
| 8 | +2. Default project is changed via CLI/API |
| 9 | +3. MCP tools should immediately use the new project (no restart needed) |
| 10 | +4. All operations should work consistently in the new project context |
| 11 | +""" |
| 12 | + |
| 13 | +import pytest |
| 14 | +from fastmcp import Client |
| 15 | + |
| 16 | + |
| 17 | +@pytest.mark.asyncio |
| 18 | +async def test_project_state_sync_after_default_change(mcp_server, app, config_manager): |
| 19 | + """Test that MCP session stays in sync when default project is changed.""" |
| 20 | + |
| 21 | + async with Client(mcp_server) as client: |
| 22 | + # Step 1: Verify initial state - MCP should show test-project as current |
| 23 | + initial_result = await client.call_tool("get_current_project", {}) |
| 24 | + assert len(initial_result) == 1 |
| 25 | + assert "Current project: test-project" in initial_result[0].text |
| 26 | + |
| 27 | + # Step 2: Create a second project that we can switch to |
| 28 | + create_result = await client.call_tool( |
| 29 | + "create_memory_project", |
| 30 | + { |
| 31 | + "project_name": "minerva", |
| 32 | + "project_path": "/tmp/minerva-test-project", |
| 33 | + "set_default": False, # Don't set as default yet |
| 34 | + }, |
| 35 | + ) |
| 36 | + assert len(create_result) == 1 |
| 37 | + assert "✓" in create_result[0].text |
| 38 | + assert "minerva" in create_result[0].text |
| 39 | + |
| 40 | + # Step 3: Change default project to minerva via set_default_project tool |
| 41 | + # This simulates the CLI command `basic-memory project default minerva` |
| 42 | + set_default_result = await client.call_tool( |
| 43 | + "set_default_project", {"project_name": "minerva"} |
| 44 | + ) |
| 45 | + assert len(set_default_result) == 1 |
| 46 | + assert "✓" in set_default_result[0].text |
| 47 | + assert "minerva" in set_default_result[0].text |
| 48 | + |
| 49 | + # Step 4: Verify MCP session immediately reflects the change (no restart needed) |
| 50 | + # This tests the fix - session.refresh_from_config() should have been called |
| 51 | + updated_result = await client.call_tool("get_current_project", {}) |
| 52 | + assert len(updated_result) == 1 |
| 53 | + |
| 54 | + # The fix should ensure these are consistent now: |
| 55 | + updated_text = updated_result[0].text |
| 56 | + assert "Current project: minerva" in updated_text |
| 57 | + |
| 58 | + # Step 5: Verify config manager also shows the new default |
| 59 | + assert config_manager.default_project == "minerva" |
| 60 | + |
| 61 | + # Step 6: Test that note operations work in the new project context |
| 62 | + # This validates that the identifier resolution works correctly |
| 63 | + write_result = await client.call_tool( |
| 64 | + "write_note", |
| 65 | + { |
| 66 | + "title": "Test Consistency Note", |
| 67 | + "folder": "test", |
| 68 | + "content": "# Test Note\n\nThis note tests project state consistency.\n\n- [test] Project state sync working", |
| 69 | + "tags": "test,consistency", |
| 70 | + }, |
| 71 | + ) |
| 72 | + assert len(write_result) == 1 |
| 73 | + assert "Test Consistency Note" in write_result[0].text |
| 74 | + |
| 75 | + # Step 7: Test that we can read the note we just created |
| 76 | + read_result = await client.call_tool("read_note", {"identifier": "Test Consistency Note"}) |
| 77 | + assert len(read_result) == 1 |
| 78 | + assert "Test Consistency Note" in read_result[0].text |
| 79 | + assert "project state sync working" in read_result[0].text.lower() |
| 80 | + |
| 81 | + # Step 8: Test that edit operations work (this was failing in the original issue) |
| 82 | + edit_result = await client.call_tool( |
| 83 | + "edit_note", |
| 84 | + { |
| 85 | + "identifier": "Test Consistency Note", |
| 86 | + "operation": "append", |
| 87 | + "content": "\n\n## Update\n\nEdit operation successful after project switch!", |
| 88 | + }, |
| 89 | + ) |
| 90 | + assert len(edit_result) == 1 |
| 91 | + assert "added" in edit_result[0].text.lower() and "lines" in edit_result[0].text.lower() |
| 92 | + |
| 93 | + # Step 9: Verify the edit was applied |
| 94 | + final_read_result = await client.call_tool( |
| 95 | + "read_note", {"identifier": "Test Consistency Note"} |
| 96 | + ) |
| 97 | + assert len(final_read_result) == 1 |
| 98 | + final_content = final_read_result[0].text |
| 99 | + assert "Edit operation successful" in final_content |
| 100 | + |
| 101 | + # Clean up - switch back to test-project |
| 102 | + await client.call_tool("switch_project", {"project_name": "test-project"}) |
| 103 | + |
| 104 | + |
| 105 | +@pytest.mark.asyncio |
| 106 | +async def test_multiple_project_switches_maintain_consistency(mcp_server, app, config_manager): |
| 107 | + """Test that multiple project switches maintain consistent state.""" |
| 108 | + |
| 109 | + async with Client(mcp_server) as client: |
| 110 | + # Create multiple test projects |
| 111 | + for project_name in ["project-a", "project-b", "project-c"]: |
| 112 | + await client.call_tool( |
| 113 | + "create_memory_project", |
| 114 | + { |
| 115 | + "project_name": project_name, |
| 116 | + "project_path": f"/tmp/{project_name}", |
| 117 | + "set_default": False, |
| 118 | + }, |
| 119 | + ) |
| 120 | + |
| 121 | + # Test switching between projects multiple times |
| 122 | + for project_name in ["project-a", "project-b", "project-c", "test-project"]: |
| 123 | + # Set as default |
| 124 | + set_result = await client.call_tool( |
| 125 | + "set_default_project", {"project_name": project_name} |
| 126 | + ) |
| 127 | + assert "✓" in set_result[0].text |
| 128 | + |
| 129 | + # Verify MCP session immediately reflects the change |
| 130 | + current_result = await client.call_tool("get_current_project", {}) |
| 131 | + assert f"Current project: {project_name}" in current_result[0].text |
| 132 | + |
| 133 | + # Verify config is also updated |
| 134 | + assert config_manager.default_project == project_name |
| 135 | + |
| 136 | + # Test that operations work in this project |
| 137 | + note_title = f"Note in {project_name}" |
| 138 | + write_result = await client.call_tool( |
| 139 | + "write_note", |
| 140 | + { |
| 141 | + "title": note_title, |
| 142 | + "folder": "test", |
| 143 | + "content": f"# {note_title}\n\nTesting operations in {project_name}.", |
| 144 | + "tags": "test", |
| 145 | + }, |
| 146 | + ) |
| 147 | + assert note_title in write_result[0].text |
| 148 | + |
| 149 | + # Clean up - switch back to test-project |
| 150 | + await client.call_tool("set_default_project", {"project_name": "test-project"}) |
| 151 | + |
| 152 | + |
| 153 | +@pytest.mark.asyncio |
| 154 | +async def test_session_handles_nonexistent_project_gracefully(mcp_server, app): |
| 155 | + """Test that session handles attempts to switch to nonexistent projects gracefully.""" |
| 156 | + |
| 157 | + async with Client(mcp_server) as client: |
| 158 | + # Try to switch to a project that doesn't exist |
| 159 | + switch_result = await client.call_tool( |
| 160 | + "switch_project", {"project_name": "nonexistent-project"} |
| 161 | + ) |
| 162 | + assert len(switch_result) == 1 |
| 163 | + result_text = switch_result[0].text |
| 164 | + |
| 165 | + # Should show an error message |
| 166 | + assert "Error:" in result_text |
| 167 | + assert "not found" in result_text.lower() |
| 168 | + assert "Available projects:" in result_text |
| 169 | + assert "test-project" in result_text # Should list available projects |
| 170 | + |
| 171 | + # Verify the session stays on the original project |
| 172 | + current_result = await client.call_tool("get_current_project", {}) |
| 173 | + assert "Current project: test-project" in current_result[0].text |
0 commit comments