@@ -1453,6 +1453,60 @@ async def test_resolve_project_and_path_preserves_existing_project_prefixed_memo
14531453 assert is_memory_url is True
14541454
14551455
1456+ @pytest .mark .asyncio
1457+ async def test_resolve_project_and_path_uses_cached_workspace_for_active_route (
1458+ config_manager ,
1459+ monkeypatch ,
1460+ ):
1461+ from mcp .server .fastmcp .exceptions import ToolError
1462+
1463+ import basic_memory .mcp .project_context as project_context
1464+ from basic_memory .mcp .project_context import resolve_project_and_path
1465+ from basic_memory .schemas .project_info import ProjectItem
1466+
1467+ config = config_manager .load_config ()
1468+ config .permalinks_include_project = True
1469+ config_manager .save_config (config )
1470+
1471+ context = _ContextState ()
1472+ cached_project = ProjectItem (
1473+ id = 1 ,
1474+ external_id = "11111111-1111-1111-1111-111111111111" ,
1475+ name = "main" ,
1476+ path = "/tmp/main" ,
1477+ is_default = False ,
1478+ )
1479+ team_workspace = _workspace (
1480+ tenant_id = "team-tenant" ,
1481+ workspace_type = "organization" ,
1482+ slug = "team-paul" ,
1483+ name = "Team Paul" ,
1484+ role = "editor" ,
1485+ )
1486+ await context .set_state ("active_project" , cached_project .model_dump ())
1487+ await context .set_state ("active_workspace" , team_workspace .model_dump ())
1488+
1489+ async def fake_call_post (* args , ** kwargs ):
1490+ raise ToolError ("project not found" )
1491+
1492+ async def fake_get_active_project (* args , ** kwargs ):
1493+ return cached_project
1494+
1495+ monkeypatch .setattr ("basic_memory.mcp.tools.utils.call_post" , fake_call_post )
1496+ monkeypatch .setattr (project_context , "get_active_project" , fake_get_active_project )
1497+
1498+ active_project , resolved_path , is_memory_url = await resolve_project_and_path (
1499+ client = cast (Any , None ),
1500+ identifier = "memory://notes/foo" ,
1501+ project = "main" ,
1502+ context = _ctx (context ),
1503+ )
1504+
1505+ assert active_project == cached_project
1506+ assert resolved_path == "team-paul/main/notes/foo"
1507+ assert is_memory_url is True
1508+
1509+
14561510class TestDetectProjectFromUrlPrefix :
14571511 """Test detect_project_from_url_prefix for URL-based project detection."""
14581512
@@ -1556,6 +1610,92 @@ async def test_local_flag_skips_workspace_resolution(self, config_manager, monke
15561610 # The error should NOT be about workspaces
15571611 assert "workspace" not in str (exc_info .value ).lower ()
15581612
1613+ @pytest .mark .asyncio
1614+ async def test_local_route_clears_stale_cached_workspace (self , config_manager , monkeypatch ):
1615+ """A previous cloud workspace must not decorate later local memory URLs."""
1616+ from contextlib import asynccontextmanager
1617+
1618+ from mcp .server .fastmcp .exceptions import ToolError
1619+
1620+ import basic_memory .mcp .project_context as project_context
1621+ from basic_memory .config import ProjectEntry
1622+ from basic_memory .mcp .project_context import get_project_client , resolve_project_and_path
1623+ from basic_memory .schemas .project_info import ProjectItem
1624+ from basic_memory .workspace_context import current_workspace_permalink_context
1625+
1626+ config = config_manager .load_config ()
1627+ config .permalinks_include_project = True
1628+ config .projects ["local-proj" ] = ProjectEntry (
1629+ path = str (config_manager .config_dir .parent / "local-proj" )
1630+ )
1631+ config_manager .save_config (config )
1632+
1633+ context = _ContextState ()
1634+ stale_workspace = _workspace (
1635+ tenant_id = "team-tenant" ,
1636+ workspace_type = "organization" ,
1637+ slug = "team-paul" ,
1638+ name = "Team Paul" ,
1639+ role = "editor" ,
1640+ )
1641+ await context .set_state ("active_workspace" , stale_workspace .model_dump ())
1642+
1643+ seen : dict [str , object ] = {}
1644+ active = ProjectItem (
1645+ id = 1 ,
1646+ external_id = "local-project-id" ,
1647+ name = "local-proj" ,
1648+ path = "/local-proj" ,
1649+ is_default = False ,
1650+ )
1651+
1652+ async def fail_ensure_workspace_project_index (context = None ): # pragma: no cover
1653+ raise AssertionError ("Local routing must not discover cloud workspaces" )
1654+
1655+ @asynccontextmanager
1656+ async def fake_get_client (project_name = None , workspace = None ):
1657+ seen ["project_name" ] = project_name
1658+ seen ["workspace" ] = workspace
1659+ seen ["permalink_context" ] = current_workspace_permalink_context ()
1660+ yield object ()
1661+
1662+ async def fake_get_active_project (client , project_name , context = None , headers = None ):
1663+ assert project_name == "local-proj"
1664+ return active
1665+
1666+ async def fake_call_post (* args , ** kwargs ):
1667+ raise ToolError ("project not found" )
1668+
1669+ monkeypatch .setattr (
1670+ project_context ,
1671+ "_ensure_workspace_project_index" ,
1672+ fail_ensure_workspace_project_index ,
1673+ )
1674+ monkeypatch .setattr ("basic_memory.mcp.async_client.get_client" , fake_get_client )
1675+ monkeypatch .setattr (project_context , "get_active_project" , fake_get_active_project )
1676+ monkeypatch .setattr ("basic_memory.mcp.tools.utils.call_post" , fake_call_post )
1677+
1678+ async with get_project_client (
1679+ project = "local-proj" ,
1680+ context = _ctx (context ),
1681+ ) as (client , active_project ):
1682+ _ , resolved_path , is_memory_url = await resolve_project_and_path (
1683+ client = cast (Any , client ),
1684+ identifier = "memory://notes/foo" ,
1685+ project = "local-proj" ,
1686+ context = _ctx (context ),
1687+ )
1688+
1689+ assert active_project == active
1690+ assert seen == {
1691+ "project_name" : "local-proj" ,
1692+ "workspace" : None ,
1693+ "permalink_context" : None ,
1694+ }
1695+ assert resolved_path == "local-proj/notes/foo"
1696+ assert is_memory_url is True
1697+ assert await context .get_state ("active_workspace" ) is None
1698+
15591699 @pytest .mark .asyncio
15601700 async def test_cloud_project_uses_per_project_workspace_id (self , config_manager , monkeypatch ):
15611701 """Cloud project with workspace_id uses cached workspace permalink context."""
0 commit comments