forked from HAL9000/cleveragents-core
d0689573e0
Add TDD regression tests for bug #570 where `_get_session_service()` calls `container.db()` but the DI `Container` class has no `db` provider, raising `AttributeError`. Same root cause as bug #554. Includes 4 Behave BDD scenarios tagged `@tdd_bug @tdd_bug_570 @tdd_expected_fail`, Robot Framework integration smoke tests with `--format plain`, and ASV service-layer benchmarks. Tests exercise the real DI path by resetting `_service = None` and using a file-based SQLite database. Implements the `@tdd_expected_fail` inversion infrastructure: - Behave: `after_scenario` hook in `features/environment.py` inverts pass/fail for scenarios tagged `@tdd_expected_fail` - Robot: `robot/tdd_expected_fail_listener.py` listener (API v3) performs the same inversion for Robot test cases - `noxfile.py`: registers the listener via `--listener` in both the `integration_tests` and `slow_integration_tests` sessions Migrates 18 existing TDD scenarios across 5 feature files from the old `@tdd @bugNNN` convention to the standardised `@tdd_bug @tdd_bug_NNN` tags per CONTRIBUTING.md § TDD Bug Test Tags. Refs: #570
187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
"""Step definitions for session_create_error.feature (bug #570).
|
|
|
|
TDD regression tests for ``agents session create`` after ``agents init``.
|
|
These scenarios assert the correct expected behaviour and will fail until
|
|
the DI container fix is applied.
|
|
|
|
Design rationale
|
|
~~~~~~~~~~~~~~~~
|
|
``_get_session_service()`` calls ``container.db()`` but the DI ``Container``
|
|
class has no ``db`` provider, raising ``AttributeError``. Same root cause
|
|
as bug #554.
|
|
|
|
We reset ``_service`` to ``None`` so the real ``_get_session_service()`` is
|
|
exercised. A file-based SQLite database and ``CLEVERAGENTS_DATABASE_URL``
|
|
override ensure the commands can reach the database once the fix lands.
|
|
|
|
All CLI invocations use ``--format plain`` so output goes through
|
|
``typer.echo`` (captured by ``CliRunner``) rather than ``rich.console``.
|
|
|
|
Private API access (``session_mod._service``, ``session_mod._reset_session_service``)
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
These step definitions access private attributes of the ``session`` CLI
|
|
module because the DI integration tests *must* force the module to re-run
|
|
its service resolution logic. The module caches a singleton
|
|
``_service`` instance; resetting it to ``None`` is the only way to make
|
|
the CLI re-exercise ``_get_session_service()`` (the buggy code path).
|
|
If the module's internal caching mechanism changes, these tests will
|
|
need to be updated accordingly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from sqlalchemy import create_engine
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.application.container import reset_container
|
|
from cleveragents.cli.commands import session as session_mod
|
|
from cleveragents.cli.commands.session import app as session_app
|
|
from cleveragents.infrastructure.database.models import Base
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _setup_real_di_path(context: Any) -> None:
|
|
"""Prepare a temp dir with a fresh SQLite DB and override container.
|
|
|
|
Registers cleanup immediately after capturing original state so that
|
|
env vars and module state are restored even if later setup lines raise.
|
|
"""
|
|
# Store original _service so cleanup can restore it.
|
|
context.sce_original_service = session_mod._service
|
|
context.sce_tmpdir = tempfile.mkdtemp(prefix="session_create_err_570_")
|
|
|
|
# Register cleanup early so env/state is always restored (SEC-3).
|
|
context.add_cleanup(_cleanup_sce, context)
|
|
|
|
context.sce_db_path = os.path.join(context.sce_tmpdir, "test.db")
|
|
db_url = f"sqlite:///{context.sce_db_path}"
|
|
|
|
# Create schema so the DB file exists with all tables.
|
|
engine = create_engine(db_url, echo=False)
|
|
Base.metadata.create_all(engine)
|
|
engine.dispose()
|
|
|
|
# Override the container's database_url so real DI can find the DB.
|
|
os.environ["CLEVERAGENTS_DATABASE_URL"] = db_url
|
|
|
|
# Reset global DI container so post-fix DI reads the fresh env var.
|
|
reset_container()
|
|
|
|
# Reset the module-level _service so _get_session_service() is used.
|
|
# This direct attribute mutation is fragile — if the module's internal
|
|
# caching mechanism changes (e.g. lazy singleton via descriptor), this
|
|
# line will need to be updated. See module docstring for rationale.
|
|
session_mod._service = None
|
|
|
|
context.sce_result = None
|
|
context.sce_list_result = None
|
|
|
|
|
|
def _cleanup_sce(context: Any) -> None:
|
|
"""Remove temp dir, restore env and original _service.
|
|
|
|
Also resets the module-level ``_service`` cache via the public API so
|
|
that any DI-created engine is released before the temp dir is removed.
|
|
"""
|
|
# Ensure module-level cache is cleared; GC will release any engine when
|
|
# the container is reset (done separately via reset_container()).
|
|
session_mod._reset_session_service()
|
|
# Then restore the original _service value.
|
|
session_mod._service = context.sce_original_service
|
|
# Release any DI-created connections before removing temp dir.
|
|
reset_container()
|
|
os.environ.pop("CLEVERAGENTS_DATABASE_URL", None)
|
|
shutil.rmtree(context.sce_tmpdir, ignore_errors=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a session-create-error CLI runner using the real DI path")
|
|
def step_session_create_error_runner(context: Any) -> None:
|
|
_setup_real_di_path(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When - create
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I invoke session-create-error create with no arguments")
|
|
def step_invoke_create_no_args(context: Any) -> None:
|
|
context.sce_result = runner.invoke(session_app, ["create", "--format", "plain"])
|
|
|
|
|
|
@when('I invoke session-create-error create with actor "{actor}"')
|
|
def step_invoke_create_with_actor(context: Any, actor: str) -> None:
|
|
context.sce_result = runner.invoke(
|
|
session_app, ["create", "--actor", actor, "--format", "plain"]
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When - list (for persistence verification)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I invoke session-create-error list to verify persistence")
|
|
def step_invoke_list_after_create(context: Any) -> None:
|
|
context.sce_list_result = runner.invoke(session_app, ["list", "--format", "plain"])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then - assertions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the session-create-error command should exit successfully")
|
|
def step_exit_success(context: Any) -> None:
|
|
result = context.sce_result
|
|
assert result is not None, "No command was invoked"
|
|
assert result.exit_code == 0, (
|
|
f"Expected exit code 0, got {result.exit_code}.\n"
|
|
f"Output: {result.output}\n"
|
|
f"Exception: {result.exception!r}"
|
|
)
|
|
|
|
|
|
@then('the session-create-error output should contain "{text}"')
|
|
def step_output_contains(context: Any, text: str) -> None:
|
|
result = context.sce_result
|
|
assert result is not None, "No command was invoked"
|
|
assert text in result.output, (
|
|
f"Expected '{text}' in output but got:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then("the session-create-error list should show at least one session")
|
|
def step_list_shows_sessions(context: Any) -> None:
|
|
result = context.sce_list_result
|
|
assert result is not None, "List was not invoked"
|
|
assert result.exit_code == 0, (
|
|
f"Expected list exit code 0, got {result.exit_code}.\n"
|
|
f"Output: {result.output}\n"
|
|
f"Exception: {result.exception!r}"
|
|
)
|
|
# In plain format the output should contain "total:" with a count > 0.
|
|
assert "total:" in result.output, (
|
|
f"Expected 'total:' in plain list output but got:\n{result.output}"
|
|
)
|
|
assert "total: 0" not in result.output, (
|
|
f"Expected at least one session but got:\n{result.output}"
|
|
)
|