@@ -328,6 +328,204 @@ async def fake_get_client(*, workspace=None, project_name=None):
328328 assert captured ["workspace" ] is None
329329
330330
331+ @pytest .mark .asyncio
332+ async def test_delete_project_resolves_workspace_slug (app ):
333+ """A friendly workspace slug resolves to the tenant id used for delete routing."""
334+ from contextlib import asynccontextmanager
335+ import httpx
336+
337+ from basic_memory .mcp .clients import ProjectClient
338+ from basic_memory .schemas .project_info import ProjectStatusResponse
339+
340+ target_project = _make_project (
341+ "WS Project" ,
342+ "/ws-project" ,
343+ external_id = "project-uuid" ,
344+ )
345+ captured : dict [str , str | None ] = {}
346+
347+ @asynccontextmanager
348+ async def fake_get_client (* , workspace = None , project_name = None ):
349+ captured ["workspace" ] = workspace
350+ async with httpx .AsyncClient (base_url = "http://testserver" ) as client :
351+ yield client
352+
353+ fake_status = ProjectStatusResponse (
354+ message = "Project deleted" ,
355+ status = "success" ,
356+ default = False ,
357+ old_project = target_project ,
358+ )
359+
360+ with (
361+ patch (
362+ "basic_memory.mcp.tools.project_management.get_client" ,
363+ new = fake_get_client ,
364+ ),
365+ patch (
366+ "basic_memory.mcp.tools.project_management.is_factory_mode" ,
367+ return_value = True ,
368+ ),
369+ patch (
370+ "basic_memory.mcp.tools.project_management.resolve_workspace_parameter" ,
371+ new_callable = AsyncMock ,
372+ return_value = _make_workspace (
373+ "tenant-abc-123" ,
374+ "Team Paul" ,
375+ workspace_type = "organization" ,
376+ slug = "team-paul" ,
377+ ),
378+ ) as mock_resolve_workspace ,
379+ patch .object (
380+ ProjectClient ,
381+ "list_projects" ,
382+ new_callable = AsyncMock ,
383+ return_value = _make_list ([target_project ], default = None ),
384+ ),
385+ patch .object (
386+ ProjectClient ,
387+ "delete_project" ,
388+ new_callable = AsyncMock ,
389+ return_value = fake_status ,
390+ ) as mock_delete_project ,
391+ patch (
392+ "basic_memory.mcp.project_context.invalidate_workspace_project_index" ,
393+ new_callable = AsyncMock ,
394+ ),
395+ ):
396+ result = await delete_project ("WS Project" , workspace = "team-paul" )
397+
398+ mock_resolve_workspace .assert_awaited_once_with (workspace = "team-paul" , context = None )
399+ assert captured ["workspace" ] == "tenant-abc-123"
400+ mock_delete_project .assert_awaited_once_with ("project-uuid" )
401+ assert result .startswith ("✓" )
402+
403+
404+ @pytest .mark .asyncio
405+ async def test_delete_project_workspace_is_local_noop (app ):
406+ """Local delete accepts workspace without requiring cloud workspace discovery."""
407+ from contextlib import asynccontextmanager
408+ import httpx
409+
410+ from basic_memory .mcp .clients import ProjectClient
411+ from basic_memory .schemas .project_info import ProjectStatusResponse
412+
413+ target_project = _make_project (
414+ "Local WS Project" ,
415+ "/local-ws-project" ,
416+ external_id = "local-project-uuid" ,
417+ )
418+ captured : dict [str , str | None ] = {}
419+
420+ @asynccontextmanager
421+ async def fake_get_client (* , workspace = None , project_name = None ):
422+ captured ["workspace" ] = workspace
423+ async with httpx .AsyncClient (base_url = "http://testserver" ) as client :
424+ yield client
425+
426+ fake_status = ProjectStatusResponse (
427+ message = "Project deleted" ,
428+ status = "success" ,
429+ default = False ,
430+ old_project = target_project ,
431+ )
432+
433+ with (
434+ patch (
435+ "basic_memory.mcp.tools.project_management.get_client" ,
436+ new = fake_get_client ,
437+ ),
438+ patch (
439+ "basic_memory.mcp.tools.project_management.is_factory_mode" ,
440+ return_value = False ,
441+ ),
442+ patch (
443+ "basic_memory.mcp.tools.project_management.has_cloud_credentials" ,
444+ return_value = False ,
445+ ),
446+ patch (
447+ "basic_memory.mcp.tools.project_management.resolve_workspace_parameter" ,
448+ new_callable = AsyncMock ,
449+ ) as mock_resolve_workspace ,
450+ patch .object (
451+ ProjectClient ,
452+ "list_projects" ,
453+ new_callable = AsyncMock ,
454+ return_value = _make_list ([target_project ], default = None ),
455+ ),
456+ patch .object (
457+ ProjectClient ,
458+ "delete_project" ,
459+ new_callable = AsyncMock ,
460+ return_value = fake_status ,
461+ ),
462+ patch (
463+ "basic_memory.mcp.project_context.invalidate_workspace_project_index" ,
464+ new_callable = AsyncMock ,
465+ ),
466+ ):
467+ await delete_project ("Local WS Project" , workspace = "team-paul" )
468+
469+ mock_resolve_workspace .assert_not_awaited ()
470+ assert captured ["workspace" ] == "team-paul"
471+
472+
473+ @pytest .mark .asyncio
474+ async def test_delete_project_default_workspace_is_none (app ):
475+ """When workspace is omitted, delete_project routes through the default workspace."""
476+ from contextlib import asynccontextmanager
477+ import httpx
478+
479+ from basic_memory .mcp .clients import ProjectClient
480+ from basic_memory .schemas .project_info import ProjectStatusResponse
481+
482+ target_project = _make_project (
483+ "Default WS Project" ,
484+ "/default-ws-project" ,
485+ external_id = "default-project-uuid" ,
486+ )
487+ captured : dict [str , str | None ] = {"workspace" : "sentinel" }
488+
489+ @asynccontextmanager
490+ async def fake_get_client (* , workspace = None , project_name = None ):
491+ captured ["workspace" ] = workspace
492+ async with httpx .AsyncClient (base_url = "http://testserver" ) as client :
493+ yield client
494+
495+ fake_status = ProjectStatusResponse (
496+ message = "Project deleted" ,
497+ status = "success" ,
498+ default = False ,
499+ old_project = target_project ,
500+ )
501+
502+ with (
503+ patch (
504+ "basic_memory.mcp.tools.project_management.get_client" ,
505+ new = fake_get_client ,
506+ ),
507+ patch .object (
508+ ProjectClient ,
509+ "list_projects" ,
510+ new_callable = AsyncMock ,
511+ return_value = _make_list ([target_project ], default = None ),
512+ ),
513+ patch .object (
514+ ProjectClient ,
515+ "delete_project" ,
516+ new_callable = AsyncMock ,
517+ return_value = fake_status ,
518+ ),
519+ patch (
520+ "basic_memory.mcp.project_context.invalidate_workspace_project_index" ,
521+ new_callable = AsyncMock ,
522+ ),
523+ ):
524+ await delete_project ("Default WS Project" )
525+
526+ assert captured ["workspace" ] is None
527+
528+
331529# --- Cloud merge tests ---
332530
333531
0 commit comments