diff --git a/app.py b/app.py index d1e21fe5f..b614e2135 100644 --- a/app.py +++ b/app.py @@ -704,7 +704,7 @@ from routes.diagnostics_routes import setup_diagnostics_routes app.include_router(setup_diagnostics_routes(rag_manager, rag_available, research_handler, memory_vector)) # Cleanup -from routes.cleanup_routes import setup_cleanup_routes +from routes.cleanup.cleanup_routes import setup_cleanup_routes app.include_router(setup_cleanup_routes(session_manager)) # Personal docs diff --git a/routes/cleanup/__init__.py b/routes/cleanup/__init__.py new file mode 100644 index 000000000..891d27e96 --- /dev/null +++ b/routes/cleanup/__init__.py @@ -0,0 +1,5 @@ +"""Cleanup route domain package (slice 2g, #4082/#4071). + +Contains cleanup_routes.py, migrated from the flat routes/ directory. +Backward-compat shim at routes/cleanup_routes.py re-exports from here. +""" diff --git a/routes/cleanup/cleanup_routes.py b/routes/cleanup/cleanup_routes.py new file mode 100644 index 000000000..ce1b63be0 --- /dev/null +++ b/routes/cleanup/cleanup_routes.py @@ -0,0 +1,60 @@ +# routes/cleanup_routes.py +"""Routes for cleanup operations.""" +import logging +from fastapi import APIRouter, HTTPException, Request +from src.cleanup_service import get_cleanup_preview, cleanup_sessions +from src.auth_helpers import get_current_user + +logger = logging.getLogger(__name__) + +def setup_cleanup_routes(session_manager): + """ + Setup cleanup-related routes. + + Args: + session_manager: SessionManager instance + + Returns: + APIRouter instance with cleanup routes + """ + router = APIRouter(prefix="/api/cleanup") + + @router.get("/preview") + async def cleanup_preview(request: Request): + """ + Preview what would be cleaned up without making any changes. + + Returns: + JSON response with lists of sessions that would be archived/deleted and estimated space savings + """ + user = get_current_user(request) + try: + preview = await get_cleanup_preview(owner=user) + return preview + except Exception as e: + logger.error(f"Cleanup preview failed: {e}") + raise HTTPException(500, "Cleanup preview generation failed") + + @router.post("") + async def cleanup_endpoint(request: Request): + """ + Perform cleanup operations: + 1. Archive inactive sessions (not accessed for 7 days) + 2. Delete old sessions (archived, not important, not accessed for 14+ days, with fewer than 10 messages) + + Returns: + JSON response with counts of deleted and archived sessions, and space freed + """ + user = get_current_user(request) + try: + archived_count, deleted_count, space_freed_mb = await cleanup_sessions(session_manager, owner=user) + return { + "archived_count": archived_count, + "deleted_count": deleted_count, + "space_freed_mb": round(space_freed_mb, 2) + } + except Exception as e: + logger.error(f"Cleanup failed: {e}") + raise HTTPException(500, "Cleanup operation failed") + + return router diff --git a/routes/cleanup_routes.py b/routes/cleanup_routes.py index ce1b63be0..1639ca85d 100644 --- a/routes/cleanup_routes.py +++ b/routes/cleanup_routes.py @@ -1,60 +1,17 @@ -# routes/cleanup_routes.py -"""Routes for cleanup operations.""" -import logging -from fastapi import APIRouter, HTTPException, Request -from src.cleanup_service import get_cleanup_preview, cleanup_sessions -from src.auth_helpers import get_current_user +"""Backward-compat shim — canonical location is routes/cleanup/cleanup_routes.py. -logger = logging.getLogger(__name__) +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.cleanup_routes``, ``from routes.cleanup_routes import X``, +``importlib.import_module("routes.cleanup_routes")``, and the string-targeted +``monkeypatch.setattr("routes.cleanup_routes.get_cleanup_preview", ...)`` / +``"routes.cleanup_routes.get_current_user"`` / ``"routes.cleanup_routes. +cleanup_sessions"`` pattern used by test_cleanup_owner_scope.py all operate +on the *same* object the application actually uses. Keeps existing import +paths working after slice 2g (#4082/#4071). +""" -def setup_cleanup_routes(session_manager): - """ - Setup cleanup-related routes. +import sys as _sys - Args: - session_manager: SessionManager instance +from routes.cleanup import cleanup_routes as _canonical # noqa: F401 - Returns: - APIRouter instance with cleanup routes - """ - router = APIRouter(prefix="/api/cleanup") - - @router.get("/preview") - async def cleanup_preview(request: Request): - """ - Preview what would be cleaned up without making any changes. - - Returns: - JSON response with lists of sessions that would be archived/deleted and estimated space savings - """ - user = get_current_user(request) - try: - preview = await get_cleanup_preview(owner=user) - return preview - except Exception as e: - logger.error(f"Cleanup preview failed: {e}") - raise HTTPException(500, "Cleanup preview generation failed") - - @router.post("") - async def cleanup_endpoint(request: Request): - """ - Perform cleanup operations: - 1. Archive inactive sessions (not accessed for 7 days) - 2. Delete old sessions (archived, not important, not accessed for 14+ days, with fewer than 10 messages) - - Returns: - JSON response with counts of deleted and archived sessions, and space freed - """ - user = get_current_user(request) - try: - archived_count, deleted_count, space_freed_mb = await cleanup_sessions(session_manager, owner=user) - return { - "archived_count": archived_count, - "deleted_count": deleted_count, - "space_freed_mb": round(space_freed_mb, 2) - } - except Exception as e: - logger.error(f"Cleanup failed: {e}") - raise HTTPException(500, "Cleanup operation failed") - - return router +_sys.modules[__name__] = _canonical diff --git a/tests/test_cleanup_routes_shim.py b/tests/test_cleanup_routes_shim.py new file mode 100644 index 000000000..dff16eb3b --- /dev/null +++ b/tests/test_cleanup_routes_shim.py @@ -0,0 +1,25 @@ +"""Regression test for the cleanup route shim (slice 2g, #4082/#4071). + +The backward-compat shim at ``routes/cleanup_routes.py`` uses ``sys.modules`` +replacement so the legacy import path and the canonical ``routes.cleanup.*`` +path resolve to the *same* module object. This is required because +``test_cleanup_owner_scope.py`` uses string-targeted +``monkeypatch.setattr("routes.cleanup_routes.get_cleanup_preview", ...)`` and +``monkeypatch.delitem(sys.modules, "routes.cleanup_routes")`` + re-import — +for those patches to take effect at runtime, the legacy module object and +the canonical one must be identical. +""" + +import importlib + +import routes.cleanup_routes as _shim_cleanup # noqa: F401 + + +def test_legacy_and_canonical_cleanup_module_are_same_object(): + """``import routes.cleanup_routes`` must alias the canonical module.""" + legacy = importlib.import_module("routes.cleanup_routes") + canonical = importlib.import_module("routes.cleanup.cleanup_routes") + assert legacy is canonical, ( + "routes.cleanup_routes shim must resolve to the canonical " + "routes.cleanup.cleanup_routes module object" + )