Files
cleveragents-core/benchmarks/session_list_bench.py
Brent E. Edwards 4c5589da4e test(cli): add failing TDD tests for session list DI container error
Add 10 Behave BDD scenarios (@tdd_bug @tdd_bug_554 @tdd_expected_fail)
for the session list DI wiring bug where _get_session_service() calls
container.db() but the Container has no db provider (AttributeError).
Scenarios cover empty list, format validation (JSON/YAML/plain/rich),
init-then-list lifecycle, and post-create list paths.

Implement @tdd_expected_fail infrastructure: Behave after_scenario hook
inverts FAIL→PASS (expected) and PASS→FAIL (unexpected fix), plus Robot
Framework listener (Listener API v3) with identical semantics registered
via --listener in both integration_tests and slow_integration_tests nox
sessions.

Migrate 18 existing TDD scenarios across 5 feature files from legacy
@tdd @bugNNN convention to @tdd_bug @tdd_bug_NNN per CONTRIBUTING.md
§ TDD Bug Test Tags.

Includes Robot Framework integration smoke tests and ASV service-layer
benchmarks.

Refs: #554
2026-03-10 21:24:42 +00:00

158 lines
5.5 KiB
Python

"""ASV benchmarks for session list service-layer performance baseline (bug #554).
Measures the cost of listing sessions through ``PersistentSessionService``
using a file-based SQLite database. These benchmarks construct the service
directly (bypassing DI wiring) and therefore do **not** exercise the
``_get_session_service()`` code path that triggers bug #554. Their purpose
is to establish a performance baseline for the service layer itself.
"""
from __future__ import annotations
import os
import shutil
import sys
import tempfile
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from sqlalchemy import create_engine # noqa: E402
from sqlalchemy.orm import sessionmaker # noqa: E402
from cleveragents.application.services.session_service import ( # noqa: E402
PersistentSessionService,
)
from cleveragents.infrastructure.database.models import Base # noqa: E402
from cleveragents.infrastructure.database.repositories import ( # noqa: E402
SessionMessageRepository,
SessionRepository,
)
class SessionListDISuite:
"""Benchmark session list through the service layer (direct construction).
Engine and sessionmaker are built once in ``setup()`` and reused across
``time_list_empty`` iterations. Methods that mutate state
(``time_list_after_create``, ``track_list_after_create_count``) use
per-method setup/teardown to get a fresh database each time, preventing
row accumulation across ASV iterations.
Does **not** exercise ``_get_session_service()`` or the DI container
wiring.
"""
timeout = 30.0
# -- suite-level setup (shared engine for read-only benchmarks) ----------
def setup(self) -> None:
self._tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_")
self._db_path = os.path.join(self._tmpdir, "bench.db")
self._engine = create_engine(
f"sqlite:///{self._db_path}",
echo=False,
)
Base.metadata.create_all(self._engine)
self._session_factory = sessionmaker(
bind=self._engine,
expire_on_commit=False,
)
def teardown(self) -> None:
self._engine.dispose()
shutil.rmtree(self._tmpdir, ignore_errors=True)
def _make_service(self) -> PersistentSessionService:
"""Build a PersistentSessionService using the shared session factory."""
return PersistentSessionService(
session_repo=SessionRepository(
session_factory=self._session_factory,
),
message_repo=SessionMessageRepository(
session_factory=self._session_factory,
),
)
# -- per-method setup for mutating benchmarks ----------------------------
def _fresh_engine(self) -> None:
"""Create an isolated engine+factory so create() doesn't accumulate."""
self._mut_tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_mut_")
db_path = os.path.join(self._mut_tmpdir, "bench.db")
self._mut_engine = create_engine(
f"sqlite:///{db_path}",
echo=False,
)
Base.metadata.create_all(self._mut_engine)
self._mut_factory = sessionmaker(
bind=self._mut_engine,
expire_on_commit=False,
)
def _dispose_fresh(self) -> None:
if hasattr(self, "_mut_engine"):
self._mut_engine.dispose()
if hasattr(self, "_mut_tmpdir"):
shutil.rmtree(self._mut_tmpdir, ignore_errors=True)
def _make_mut_service(self) -> PersistentSessionService:
return PersistentSessionService(
session_repo=SessionRepository(session_factory=self._mut_factory),
message_repo=SessionMessageRepository(
session_factory=self._mut_factory,
),
)
# -- ASV per-method hooks ------------------------------------------------
def setup_time_list_after_create(self) -> None:
self._fresh_engine()
def teardown_time_list_after_create(self) -> None:
self._dispose_fresh()
def setup_track_list_after_create_count(self) -> None:
self._fresh_engine()
def teardown_track_list_after_create_count(self) -> None:
self._dispose_fresh()
def setup_time_list_empty(self) -> None:
self._empty_svc = self._make_service()
# -- benchmarks ----------------------------------------------------------
def time_list_empty(self) -> None:
"""List sessions when DB is empty (service-layer only, no DI)."""
self._empty_svc.list()
def time_list_after_create(self) -> None:
"""Create then list (service-layer only, fresh DB per iteration).
Bypasses ``_get_session_service()`` / DI container — measures the
service-layer round-trip cost, not the DI wiring path.
"""
svc = self._make_mut_service()
svc.create()
svc.list()
def track_list_after_create_count(self) -> int:
"""Track session list persistence at the service layer.
Returns the count of sessions visible after a create. Uses a
fresh DB per iteration so the count is always exactly 1.
This benchmark constructs ``PersistentSessionService`` directly,
bypassing the DI container — it does **not** reproduce bug #554.
"""
svc = self._make_mut_service()
svc.create(actor_name="bench/test")
sessions = svc.list()
return len(sessions)
SessionListDISuite.track_list_after_create_count.unit = "sessions"