@@ -1717,6 +1717,134 @@ async def fake_get_active_project(client, project_name, context=None, headers=No
17171717 assert permalink_context .workspace_slug == "team-paul"
17181718 assert permalink_context .workspace_type == "organization"
17191719
1720+ @pytest .mark .asyncio
1721+ async def test_cloud_project_workspace_id_uses_cached_available_workspaces_for_permalink_context (
1722+ self , config_manager , monkeypatch
1723+ ):
1724+ """Cached workspace metadata supplies slug/type before the provider is consulted."""
1725+ from contextlib import asynccontextmanager
1726+
1727+ import basic_memory .mcp .project_context as project_context
1728+ from basic_memory .mcp .project_context import get_project_client
1729+ from basic_memory .config import ProjectEntry , ProjectMode
1730+ from basic_memory .schemas .project_info import ProjectItem
1731+ from basic_memory .workspace_context import (
1732+ WorkspacePermalinkContext ,
1733+ current_workspace_permalink_context ,
1734+ )
1735+
1736+ config = config_manager .load_config ()
1737+ config .projects ["cloud-proj" ] = ProjectEntry (
1738+ path = str (config_manager .config_dir .parent / "cloud-proj" ),
1739+ mode = ProjectMode .CLOUD ,
1740+ workspace_id = "per-project-tenant-id" ,
1741+ )
1742+ config .cloud_api_key = "bmc_test123"
1743+ config_manager .save_config (config )
1744+
1745+ workspace = _workspace (
1746+ tenant_id = "per-project-tenant-id" ,
1747+ workspace_type = "organization" ,
1748+ slug = "team-paul" ,
1749+ name = "Team Paul" ,
1750+ role = "editor" ,
1751+ )
1752+ context = _ContextState ()
1753+ await context .set_state ("available_workspaces" , ["ignored" , workspace .model_dump ()])
1754+
1755+ async def fail_workspace_provider (): # pragma: no cover
1756+ raise AssertionError ("Cached workspace metadata should be used before provider" )
1757+
1758+ async def fail_ensure_workspace_project_index (context = None ): # pragma: no cover
1759+ raise AssertionError ("Configured workspace_id should not require project discovery" )
1760+
1761+ monkeypatch .setattr (project_context , "_workspace_provider" , fail_workspace_provider )
1762+ monkeypatch .setattr (
1763+ project_context ,
1764+ "_ensure_workspace_project_index" ,
1765+ fail_ensure_workspace_project_index ,
1766+ )
1767+
1768+ seen : dict [str , object ] = {}
1769+
1770+ @asynccontextmanager
1771+ async def fake_get_client (project_name = None , workspace = None ):
1772+ seen ["project_name" ] = project_name
1773+ seen ["workspace" ] = workspace
1774+ seen ["permalink_context" ] = current_workspace_permalink_context ()
1775+ yield object ()
1776+
1777+ async def fake_get_active_project (client , project_name , context = None , headers = None ):
1778+ return ProjectItem (
1779+ id = 1 ,
1780+ external_id = "cloud-project-id" ,
1781+ name = project_name ,
1782+ path = "/cloud-proj" ,
1783+ is_default = False ,
1784+ )
1785+
1786+ monkeypatch .setattr ("basic_memory.mcp.async_client.get_client" , fake_get_client )
1787+ monkeypatch .setattr (project_context , "get_active_project" , fake_get_active_project )
1788+
1789+ async with get_project_client (
1790+ project = "cloud-proj" ,
1791+ context = _ctx (context ),
1792+ ) as (_client , active_project ):
1793+ assert active_project .external_id == "cloud-project-id"
1794+
1795+ assert seen ["project_name" ] == "cloud-proj"
1796+ assert seen ["workspace" ] == "per-project-tenant-id"
1797+ permalink_context = cast (WorkspacePermalinkContext | None , seen ["permalink_context" ])
1798+ assert permalink_context is not None
1799+ assert permalink_context .workspace_slug == "team-paul"
1800+ assert permalink_context .workspace_type == "organization"
1801+
1802+ @pytest .mark .asyncio
1803+ async def test_cloud_project_workspace_id_errors_when_provider_omits_workspace (
1804+ self , config_manager , monkeypatch
1805+ ):
1806+ """Provider metadata must include the configured tenant before slug/type forwarding."""
1807+ import basic_memory .mcp .project_context as project_context
1808+ from basic_memory .mcp .project_context import get_project_client
1809+ from basic_memory .config import ProjectEntry , ProjectMode
1810+
1811+ config = config_manager .load_config ()
1812+ config .projects ["cloud-proj" ] = ProjectEntry (
1813+ path = str (config_manager .config_dir .parent / "cloud-proj" ),
1814+ mode = ProjectMode .CLOUD ,
1815+ workspace_id = "per-project-tenant-id" ,
1816+ )
1817+ config .cloud_api_key = "bmc_test123"
1818+ config_manager .save_config (config )
1819+
1820+ async def fake_workspace_provider ():
1821+ return [
1822+ _workspace (
1823+ tenant_id = "other-tenant-id" ,
1824+ workspace_type = "organization" ,
1825+ slug = "other-team" ,
1826+ name = "Other Team" ,
1827+ role = "editor" ,
1828+ )
1829+ ]
1830+
1831+ async def fail_ensure_workspace_project_index (context = None ): # pragma: no cover
1832+ raise AssertionError ("Configured workspace_id should not require project discovery" )
1833+
1834+ monkeypatch .setattr (project_context , "_workspace_provider" , fake_workspace_provider )
1835+ monkeypatch .setattr (
1836+ project_context ,
1837+ "_ensure_workspace_project_index" ,
1838+ fail_ensure_workspace_project_index ,
1839+ )
1840+
1841+ with pytest .raises (
1842+ ValueError ,
1843+ match = "Configured workspace_id 'per-project-tenant-id' was not returned" ,
1844+ ):
1845+ async with get_project_client (project = "cloud-proj" ):
1846+ pass
1847+
17201848 @pytest .mark .asyncio
17211849 async def test_cloud_project_workspace_id_routes_without_discovery (
17221850 self , config_manager , monkeypatch
0 commit comments