Unify workspace creation and add minimal-mode contract tests
This commit is contained in:
@@ -13,7 +13,7 @@ from loguru import logger
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from services.database import init_user_database, ensure_user_workspace_db_directory
|
from services.database import WORKSPACE_DIR, init_user_database, ensure_user_workspace_db_directory
|
||||||
from services.workspace_dirs import ensure_user_workspace_dirs
|
from services.workspace_dirs import ensure_user_workspace_dirs
|
||||||
|
|
||||||
class UserWorkspaceManager:
|
class UserWorkspaceManager:
|
||||||
@@ -21,16 +21,8 @@ class UserWorkspaceManager:
|
|||||||
|
|
||||||
def __init__(self, db_session: Session):
|
def __init__(self, db_session: Session):
|
||||||
self.db = db_session
|
self.db = db_session
|
||||||
# Use environment-safe paths for production
|
# Always rely on canonical workspace root used by services.database.
|
||||||
if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"):
|
self.base_workspace_dir = Path(WORKSPACE_DIR)
|
||||||
# In production, use temp directories or skip file operations
|
|
||||||
self.base_workspace_dir = Path("/tmp/alwrity_workspace")
|
|
||||||
self.user_workspaces_dir = self.base_workspace_dir / "users"
|
|
||||||
else:
|
|
||||||
# In development, use project root 'workspace' directory
|
|
||||||
# services/user_workspace_manager.py -> services -> backend -> root
|
|
||||||
root_dir = Path(__file__).parent.parent.parent
|
|
||||||
self.base_workspace_dir = root_dir / "workspace"
|
|
||||||
self.user_workspaces_dir = self.base_workspace_dir
|
self.user_workspaces_dir = self.base_workspace_dir
|
||||||
|
|
||||||
def _sanitize_user_id(self, user_id: str) -> str:
|
def _sanitize_user_id(self, user_id: str) -> str:
|
||||||
@@ -49,27 +41,15 @@ class UserWorkspaceManager:
|
|||||||
|
|
||||||
# Sanitize user_id
|
# Sanitize user_id
|
||||||
safe_user_id = self._sanitize_user_id(user_id)
|
safe_user_id = self._sanitize_user_id(user_id)
|
||||||
|
|
||||||
# Check if we're in production and skip file operations if needed
|
|
||||||
if os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"):
|
|
||||||
logger.info("Production environment detected - skipping file workspace creation")
|
|
||||||
return {
|
|
||||||
"user_id": user_id,
|
|
||||||
"workspace_path": "/tmp/alwrity_workspace/users/user_" + safe_user_id,
|
|
||||||
"config": self._create_user_config(user_id),
|
|
||||||
"created_at": datetime.utcnow().isoformat(),
|
|
||||||
"production_mode": True
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create user-specific directories
|
|
||||||
# Format: workspaces/workspace_{user_id}
|
|
||||||
user_dir = self.user_workspaces_dir / f"workspace_{safe_user_id}"
|
user_dir = self.user_workspaces_dir / f"workspace_{safe_user_id}"
|
||||||
|
|
||||||
|
production_env = bool(os.getenv("RENDER") or os.getenv("RAILWAY") or os.getenv("HEROKU"))
|
||||||
|
filesystem_minimal_mode = bool(os.getenv("ALWRITY_FILESYSTEM_MINIMAL_MODE"))
|
||||||
|
mode = "filesystem_minimal" if filesystem_minimal_mode else ("production" if production_env else "development")
|
||||||
|
|
||||||
|
# Always create canonical tenant path and required subdirectories.
|
||||||
user_dir.mkdir(parents=True, exist_ok=True)
|
user_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Ensure canonical DB directory and migrate legacy layout if needed
|
|
||||||
self._ensure_workspace_db_directory(user_id)
|
self._ensure_workspace_db_directory(user_id)
|
||||||
|
|
||||||
# Create user-specific directories lazily via centralized helper
|
|
||||||
user_dir = ensure_user_workspace_dirs(
|
user_dir = ensure_user_workspace_dirs(
|
||||||
user_id,
|
user_id,
|
||||||
capabilities={"core", "content", "research", "media", "assets"},
|
capabilities={"core", "content", "research", "media", "assets"},
|
||||||
@@ -92,12 +72,20 @@ class UserWorkspaceManager:
|
|||||||
# If DB init fails, the app might not work.
|
# If DB init fails, the app might not work.
|
||||||
raise db_err
|
raise db_err
|
||||||
|
|
||||||
logger.info(f"✅ User workspace created: {user_dir}")
|
dirs_created = ["db", "assets", "media", "content", "config/user_config.json"]
|
||||||
|
logger.info(
|
||||||
|
"✅ User workspace created",
|
||||||
|
mode=mode,
|
||||||
|
workspace_path=str(user_dir),
|
||||||
|
dirs_created=dirs_created,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"workspace_path": str(user_dir),
|
"workspace_path": str(user_dir),
|
||||||
"config": config,
|
"config": config,
|
||||||
"created_at": datetime.now().isoformat()
|
"created_at": datetime.now().isoformat(),
|
||||||
|
"mode": mode,
|
||||||
|
"dirs_created": dirs_created,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
53
backend/tests/test_user_workspace_manager.py
Normal file
53
backend/tests/test_user_workspace_manager.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from services.user_workspace_manager import UserWorkspaceManager
|
||||||
|
|
||||||
|
|
||||||
|
def _configure_temp_workspace(monkeypatch, tmp_path):
|
||||||
|
workspace_root = tmp_path / "workspace"
|
||||||
|
monkeypatch.setattr("services.database.WORKSPACE_DIR", str(workspace_root))
|
||||||
|
monkeypatch.setattr("services.workspace_dirs.WORKSPACE_DIR", str(workspace_root))
|
||||||
|
monkeypatch.setattr("services.user_workspace_manager.WORKSPACE_DIR", str(workspace_root))
|
||||||
|
monkeypatch.setattr("services.user_workspace_manager.init_user_database", lambda user_id: None)
|
||||||
|
return workspace_root
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_required_contract(user_dir: Path):
|
||||||
|
assert user_dir.exists()
|
||||||
|
assert (user_dir / "db").exists()
|
||||||
|
assert (user_dir / "assets").exists()
|
||||||
|
assert (user_dir / "media").exists()
|
||||||
|
assert (user_dir / "content").exists()
|
||||||
|
assert (user_dir / "config" / "user_config.json").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_workspace_development_contract(monkeypatch, tmp_path):
|
||||||
|
workspace_root = _configure_temp_workspace(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.delenv("RENDER", raising=False)
|
||||||
|
monkeypatch.delenv("RAILWAY", raising=False)
|
||||||
|
monkeypatch.delenv("HEROKU", raising=False)
|
||||||
|
monkeypatch.delenv("ALWRITY_FILESYSTEM_MINIMAL_MODE", raising=False)
|
||||||
|
|
||||||
|
manager = UserWorkspaceManager(db_session=None)
|
||||||
|
result = manager.create_user_workspace("dev-user")
|
||||||
|
|
||||||
|
expected = workspace_root / "workspace_dev-user"
|
||||||
|
_assert_required_contract(expected)
|
||||||
|
assert result["workspace_path"] == str(expected)
|
||||||
|
assert result["mode"] == "development"
|
||||||
|
assert {"db", "assets", "media", "content", "config/user_config.json"}.issubset(set(result["dirs_created"]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_user_workspace_production_filesystem_minimal_contract(monkeypatch, tmp_path):
|
||||||
|
workspace_root = _configure_temp_workspace(monkeypatch, tmp_path)
|
||||||
|
monkeypatch.setenv("RENDER", "1")
|
||||||
|
monkeypatch.setenv("ALWRITY_FILESYSTEM_MINIMAL_MODE", "1")
|
||||||
|
|
||||||
|
manager = UserWorkspaceManager(db_session=None)
|
||||||
|
result = manager.create_user_workspace("prod-user")
|
||||||
|
|
||||||
|
expected = workspace_root / "workspace_prod-user"
|
||||||
|
_assert_required_contract(expected)
|
||||||
|
assert result["workspace_path"] == str(expected)
|
||||||
|
assert result["mode"] == "filesystem_minimal"
|
||||||
|
assert {"db", "assets", "media", "content", "config/user_config.json"}.issubset(set(result["dirs_created"]))
|
||||||
Reference in New Issue
Block a user