Unify workspace creation and add minimal-mode contract tests

This commit is contained in:
ي
2026-05-18 14:35:58 +05:30
parent 928c2f20aa
commit 882a62fa98
2 changed files with 76 additions and 35 deletions

View File

@@ -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:

View 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"]))