Files
temp/features/steps/tdd_sqlite_url_cwd_steps.py
hurui200320 1878998b7a refactor(testing): rename tdd_bug/tdd_bug_N tags to tdd_issue/tdd_issue_N
Rename the TDD tag system from tdd_bug/tdd_bug_<N> to tdd_issue/tdd_issue_<N>
across the entire codebase. The tdd_expected_fail tag is unchanged.

The TDD expected-failure workflow is not limited to bug fixes — it applies
equally to any issue type (features, tasks, refactors). The _bug suffix was
misleading and narrowed the perceived scope. The new _issue suffix accurately
reflects that the TDD tagging system applies to any Forgejo issue.

Changes span 92 files:
- features/environment.py: validate_tdd_tags(), should_invert_result(), and
  apply_tdd_inversion() updated — regex, variables, error messages
- robot/tdd_expected_fail_listener.py: _validate_tdd_tags(), _should_invert_result(),
  start_test(), end_test() updated consistently
- 33 Behave .feature files: all @tdd_bug/@tdd_bug_<N> tags renamed
- 29 Robot .robot files: all tdd_bug/tdd_bug_<N> tags renamed
- 3 Robot fixture files renamed (tdd_bug_alone, tdd_missing_tdd_bug,
  tdd_expected_fail_missing_bug_n) with content and references updated
- Tag validation tests and helpers updated (function names, command dispatch
  keys, output strings, fixture references)
- CONTRIBUTING.md: section renamed from 'TDD Bug Test Tags' to
  'TDD Issue Test Tags', all tag references and examples updated
- noxfile.py: comment references updated
- Step definition files, mock helpers, and benchmark files: docstring
  references updated

ISSUES CLOSED: #965
2026-03-27 05:58:35 +00:00

186 lines
6.9 KiB
Python

"""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}"
)