"""Step definitions for TDD Issue #1024 — SQLite DB URL CWD resolution. These steps verify that when ``CLEVERAGENTS_HOME`` is set to a temporary directory, the SQLite database file is created **inside** that directory rather than relative to the current working directory. The default ``database_url`` in ``Settings`` is ``sqlite:///cleveragents.db`` — a relative path that resolves against CWD. This breaks test isolation when ``CLEVERAGENTS_HOME`` points to a different directory. Bug: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/1024 TDD: https://git.cleverthis.com/cleveragents/cleveragents-core/issues/1034 """ from __future__ import annotations import logging import os import shutil import tempfile from pathlib import Path from behave import given, then, when from behave.runner import Context from cleveragents.application.container import get_database_url, reset_container from cleveragents.config.settings import Settings def _suppress_structlog_stdout() -> tuple[int, bool]: """Prevent structlog debug lines from contaminating CLI stdout.""" import structlog root = logging.getLogger() prev_level = root.level root.setLevel(logging.WARNING) prev_config = structlog.get_config() prev_cache: bool = prev_config.get("cache_logger_on_first_use", True) # type: ignore[assignment] structlog.configure( logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=False, ) return prev_level, prev_cache def _restore_structlog(prev_level: int, prev_cache: bool) -> None: """Undo the suppression applied by :func:`_suppress_structlog_stdout`.""" import structlog logging.getLogger().setLevel(prev_level) structlog.configure(cache_logger_on_first_use=prev_cache) def _extract_sqlite_path(url: str) -> Path | None: """Extract the file path from a SQLite URL. Handles both ``sqlite:///path`` (relative) and ``sqlite:////abs/path`` (absolute) forms. """ prefix = "sqlite:///" if not url.startswith(prefix): return None raw_path = url[len(prefix) :] if not raw_path: return None return Path(raw_path).resolve() @given("a fresh CLEVERAGENTS_HOME temp directory for DB URL testing") def step_fresh_home(context: Context) -> None: """Create an isolated CLEVERAGENTS_HOME and record the original CWD.""" # Reset singletons to avoid stale state reset_container() Settings._instance = None # type: ignore[attr-defined] prev_level, prev_cache = _suppress_structlog_stdout() context.original_cwd = Path.cwd().resolve() context.tdd_home = tempfile.mkdtemp(prefix="tdd_sqlite_url_1024_") # Remove any env vars that could override database_url context.saved_db_url = os.environ.pop("CLEVERAGENTS_DATABASE_URL", None) context.saved_test_db_url = os.environ.pop("CLEVERAGENTS_TEST_DATABASE_URL", None) os.environ["CLEVERAGENTS_HOME"] = context.tdd_home # Reset singletons so new Settings picks up the environment reset_container() Settings._instance = None # type: ignore[attr-defined] def _cleanup() -> None: # Restore env vars if context.saved_db_url is not None: os.environ["CLEVERAGENTS_DATABASE_URL"] = context.saved_db_url else: os.environ.pop("CLEVERAGENTS_DATABASE_URL", None) if context.saved_test_db_url is not None: os.environ["CLEVERAGENTS_TEST_DATABASE_URL"] = context.saved_test_db_url else: os.environ.pop("CLEVERAGENTS_TEST_DATABASE_URL", None) os.environ.pop("CLEVERAGENTS_HOME", None) reset_container() Settings._instance = None # type: ignore[attr-defined] shutil.rmtree(context.tdd_home, ignore_errors=True) _restore_structlog(prev_level, prev_cache) context.add_cleanup(_cleanup) @when("I resolve the effective database URL from Settings") def step_resolve_db_url(context: Context) -> None: """Resolve the effective database URL via the container helper.""" # Use the container's get_database_url which is what the app uses context.resolved_url = get_database_url() context.resolved_path = _extract_sqlite_path(context.resolved_url) @when("I resolve the Settings database_url default") def step_resolve_settings_default(context: Context) -> None: """Resolve the database_url via the Settings model default. Constructs a fresh Settings instance (with CLEVERAGENTS_DATABASE_URL removed) to exercise the default ``database_url`` field, then resolves the path. This tests the Settings-level default independently of the container's ``get_database_url()`` helper. """ settings = Settings() context.settings_db_url = settings.get_database_url() context.settings_db_path = _extract_sqlite_path(context.settings_db_url) @then("the resolved database path should be inside CLEVERAGENTS_HOME") def step_path_inside_home(context: Context) -> None: """Assert the database path is under CLEVERAGENTS_HOME.""" home = Path(context.tdd_home).resolve() db_path = context.resolved_path assert db_path is not None, ( f"Could not extract a file path from database URL: {context.resolved_url}" ) assert str(db_path).startswith(str(home)), ( f"Database path {db_path} is NOT inside CLEVERAGENTS_HOME {home}.\n" f"Full database URL: {context.resolved_url}" ) @then("the resolved database path should not be inside the original CWD") def step_path_not_in_cwd(context: Context) -> None: """Assert the database path is NOT under the original CWD.""" cwd = context.original_cwd db_path = context.resolved_path assert db_path is not None, ( f"Could not extract a file path from database URL: {context.resolved_url}" ) # The DB should not resolve to a child of the original CWD # (unless CLEVERAGENTS_HOME happens to be inside CWD, which it isn't # for our temp directory setup). home = Path(context.tdd_home).resolve() if not str(home).startswith(str(cwd)): # Only check if home is NOT under cwd (normal case) assert not str(db_path).startswith(str(cwd)), ( f"Database path {db_path} is inside CWD {cwd} instead of " f"CLEVERAGENTS_HOME {home}.\n" f"Full database URL: {context.resolved_url}" ) @then("the settings database path should be inside CLEVERAGENTS_HOME") def step_settings_path_inside_home(context: Context) -> None: """Assert the Settings-level database_url resolves inside CLEVERAGENTS_HOME.""" home = Path(context.tdd_home).resolve() db_path = context.settings_db_path assert db_path is not None, ( f"Could not extract a file path from Settings database URL: " f"{context.settings_db_url}" ) assert str(db_path).startswith(str(home)), ( f"Settings database path {db_path} is NOT inside " f"CLEVERAGENTS_HOME {home}.\n" f"Full Settings database URL: {context.settings_db_url}" )