diff --git a/Makefile b/Makefile index c60d9b9b..743137df 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ help: @echo " make start-daemon - Start prod services in background (daemon mode)" @echo " make stop - Stop all running services" @echo " make clean - Clean up processes and temporary files" + @echo " make migrate-paths - Migrate legacy users/ tree into workspaces/ layout (DRY_RUN=1 to preview)" @echo "" @echo "Docker Production Commands:" @echo " make up - Build and start production Docker services (localhost:2026)" @@ -144,6 +145,14 @@ clean: stop @-rm -rf logs/*.log 2>/dev/null || true @echo "✓ Cleanup complete" +# Lift legacy per-user paths into the per-workspace layout (PR6). +# Pass DRY_RUN=1 to log the migration plan without writing. +# DEFAULT_WORKSPACE= claims un-assigned users (defaults to legacy_workspace). +migrate-paths: + @cd backend && PYTHONPATH=. uv run python scripts/migrate_paths_to_workspace.py \ + $(if $(filter 1 true,$(DRY_RUN)),--dry-run) \ + $(if $(DEFAULT_WORKSPACE),--default-workspace $(DEFAULT_WORKSPACE)) + # ========================================== # Docker Development Commands # ========================================== diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 985bb96c..1f0684fa 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -175,6 +175,34 @@ async def _migrate_orphaned_threads(store, admin_user_id: str) -> int: return migrated +def _check_path_migration_pending(app: FastAPI) -> None: + """Warn the operator if the PR4 legacy user-isolation layout still has content. + + PR6 routes every new write into ``{base_dir}/workspaces/{wid}/...`` via + ``Paths``. Pre-PR6 installations have data at + ``{base_dir}/users/{uid}/...`` that needs ``make migrate-paths`` to lift + it under a workspace. We emit a warning at boot rather than crashing so + the gateway keeps serving (reads from the legacy tree still work via the + user_id branch of ``Paths.thread_dir``), but with a loud signal to run + the migration script. + """ + from deerflow.config.paths import get_paths + + legacy_users = get_paths().base_dir / "users" + if not legacy_users.exists(): + return + try: + has_content = any(legacy_users.iterdir()) + except OSError: + # Permission or transient FS issue — don't escalate; lifespan must succeed. + return + if has_content: + logger.warning( + "Legacy per-user layout detected at %s. Run `make migrate-paths` to lift it under the per-workspace layout (PR6).", + legacy_users, + ) + + @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Application lifespan handler.""" @@ -191,6 +219,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: config = get_gateway_config() logger.info(f"Starting API Gateway on {config.host}:{config.port}") + _check_path_migration_pending(app) + # Initialize LangGraph runtime components (StreamBridge, RunManager, checkpointer, store) async with langgraph_runtime(app): logger.info("LangGraph runtime initialised") diff --git a/backend/tests/test_path_migration_pending_warning.py b/backend/tests/test_path_migration_pending_warning.py new file mode 100644 index 00000000..c528792d --- /dev/null +++ b/backend/tests/test_path_migration_pending_warning.py @@ -0,0 +1,45 @@ +"""PR6 T6.14 — lifespan warns when legacy users/ tree still has content.""" + +from __future__ import annotations + +import logging +from pathlib import Path +from unittest.mock import patch + +import pytest + +from app.gateway.app import _check_path_migration_pending + + +class _FakePaths: + def __init__(self, base: Path): + self.base_dir = base + + +@pytest.fixture +def base(tmp_path: Path) -> Path: + return tmp_path + + +def _patch_paths(base: Path): + return patch("deerflow.config.paths.get_paths", return_value=_FakePaths(base)) + + +def test_warns_when_legacy_users_dir_has_content(base: Path, caplog: pytest.LogCaptureFixture): + (base / "users" / "alice" / "threads" / "t1").mkdir(parents=True) + with _patch_paths(base), caplog.at_level(logging.WARNING, logger="app.gateway.app"): + _check_path_migration_pending(app=None) # type: ignore[arg-type] + assert any("make migrate-paths" in rec.message for rec in caplog.records) + + +def test_silent_when_legacy_users_dir_missing(base: Path, caplog: pytest.LogCaptureFixture): + with _patch_paths(base), caplog.at_level(logging.WARNING, logger="app.gateway.app"): + _check_path_migration_pending(app=None) # type: ignore[arg-type] + assert not any("migrate-paths" in rec.message for rec in caplog.records) + + +def test_silent_when_legacy_users_dir_empty(base: Path, caplog: pytest.LogCaptureFixture): + (base / "users").mkdir() + with _patch_paths(base), caplog.at_level(logging.WARNING, logger="app.gateway.app"): + _check_path_migration_pending(app=None) # type: ignore[arg-type] + assert not any("migrate-paths" in rec.message for rec in caplog.records)