Files
temp/features/steps/session_create_error_steps.py
T
Brent E. Edwards d0689573e0 test(cli): add failing tests for session create DI container error
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
2026-03-10 21:39:31 +00:00

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