forked from HAL9000/cleveragents-core
16dc4ab18d
## Summary Fix `agents project show` not finding a project immediately after creation. Extends the `session.commit()` fix from #589 to also cover `update()` and `delete()` in `NamespacedProjectRepository`. ## Changes **Production fix** (`src/cleveragents/infrastructure/database/repositories.py`): - Add `session.commit()` to `create()`, `update()`, and `delete()` methods - Add `finally: session.close()` guard to all three methods - Update class docstring to reflect commit-per-method pattern **Tests & benchmarks**: - 3 Behave BDD regression scenarios (`features/project_show_after_create.feature`) - Robot Framework integration smoke tests with "not found" assertion (`robot/project_show_after_create.robot`) - ASV benchmarks for create-then-show round-trip (`benchmarks/project_show_after_create_bench.py`) ## Review feedback addressed - **F1**: Removed unrelated em-dash CHANGELOG edits — wrote clean entry from scratch - **F2**: Kept Suite Setup/Teardown (required for `${PYTHON}` variable); updated stale docs - **F3**: Added "not found" string assertion to Robot negative test case - **F4**: Removed redundant `Base.metadata.create_all()` from `_make_fresh_repo()` helper - Updated all stale TDD "expected to fail" comments — this PR includes the fix ## Process - Single squashed commit, rebased onto `master` (no merge commits) - Prescribed commit message from issue #590 metadata ISSUES CLOSED: #590 Reviewed-on: cleveragents/cleveragents-core#593 Reviewed-by: Rui Hu <rui.hu@cleverthis.com> Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com> Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
181 lines
6.6 KiB
Python
181 lines
6.6 KiB
Python
"""Step definitions for project_show_after_create.feature (bug #590).
|
|
|
|
Regression tests verifying that ``agents project show`` finds a project
|
|
immediately after ``agents project create`` commits it.
|
|
|
|
Root cause (fixed)
|
|
~~~~~~~~~~~~~~~~~~
|
|
Identical to bug #589. ``NamespacedProjectRepository.create()`` called
|
|
``session.flush()`` but never ``session.commit()``. The ``show`` command
|
|
obtains a fresh session via ``_get_namespaced_project_repo()`` and calls
|
|
``repo.get(name)``, which could not see the flushed-but-uncommitted row.
|
|
|
|
Test design mirrors bug #589 tests: file-based SQLite with a fresh
|
|
``NamespacedProjectRepository`` per CLI invocation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import NullPool
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands.project import app as project_app
|
|
from cleveragents.infrastructure.database.models import Base
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
NamespacedProjectRepository,
|
|
)
|
|
|
|
# Patch targets - module-level DI helpers in the project CLI.
|
|
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
|
|
_PATCH_STORE_EXTRAS = "cleveragents.cli.commands.project._store_project_extras"
|
|
|
|
# NOTE: typer.testing.CliRunner always mixes stderr into stdout
|
|
# (no mix_stderr parameter exposed). Scenario 3 relies on this
|
|
# behaviour to assert "not found" appears in result.output even
|
|
# though the CLI writes it to stderr via err_console.
|
|
runner = CliRunner()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_fresh_repo(db_path: str) -> NamespacedProjectRepository:
|
|
"""Build a *new* repo with its own engine/session for *db_path*.
|
|
|
|
``NullPool`` ensures SQLite file handles are released immediately
|
|
when the engine is no longer referenced. Schema is pre-created
|
|
once during the Background step, so no DDL is needed here.
|
|
"""
|
|
engine = create_engine(f"sqlite:///{db_path}", echo=False, poolclass=NullPool)
|
|
factory = sessionmaker(bind=engine, expire_on_commit=False)
|
|
return NamespacedProjectRepository(session_factory=factory)
|
|
|
|
|
|
def _cleanup_show_db(context: Any) -> None:
|
|
"""Remove the temporary directory."""
|
|
shutil.rmtree(context.show_tmpdir, ignore_errors=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fresh project-show database is initialised")
|
|
def step_init_show_db(context: Any) -> None:
|
|
"""Create a temp dir with a fresh SQLite file for show tests."""
|
|
context.show_tmpdir = tempfile.mkdtemp(prefix="show_590_")
|
|
context.show_db_path = os.path.join(context.show_tmpdir, "test.db")
|
|
|
|
# Pre-create schema.
|
|
engine = create_engine(
|
|
f"sqlite:///{context.show_db_path}",
|
|
echo=False,
|
|
poolclass=NullPool,
|
|
)
|
|
Base.metadata.create_all(engine)
|
|
engine.dispose()
|
|
|
|
context.show_result = None
|
|
context.add_cleanup(_cleanup_show_db, context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps - create
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I create a project named "{name}" via the project-show CLI')
|
|
def step_create_project_show(context: Any, name: str) -> None:
|
|
"""Invoke ``project create <name>`` with a fresh repo."""
|
|
repo = _make_fresh_repo(context.show_db_path)
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=repo),
|
|
patch(_PATCH_STORE_EXTRAS),
|
|
):
|
|
result = runner.invoke(project_app, ["create", name])
|
|
assert result.exit_code == 0, (
|
|
f"project create '{name}' should exit 0 but got "
|
|
f"{result.exit_code}. output:\n{result.output}"
|
|
)
|
|
|
|
|
|
@when(
|
|
'I create a described project named "{name}" with description "{desc}" '
|
|
"via the project-show CLI"
|
|
)
|
|
def step_create_project_show_desc(context: Any, name: str, desc: str) -> None:
|
|
"""Invoke ``project create <name> -d <desc>`` with a fresh repo."""
|
|
repo = _make_fresh_repo(context.show_db_path)
|
|
with (
|
|
patch(_PATCH_PROJECT_REPO, return_value=repo),
|
|
patch(_PATCH_STORE_EXTRAS),
|
|
):
|
|
result = runner.invoke(project_app, ["create", name, "-d", desc])
|
|
assert result.exit_code == 0, (
|
|
f"project create '{name}' with description should exit 0 but got "
|
|
f"{result.exit_code}. output:\n{result.output}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps - show
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('I show the project "{name}" via the project-show CLI')
|
|
def step_show_project(context: Any, name: str) -> None:
|
|
"""Invoke ``project show <name>`` with a fresh repo."""
|
|
repo = _make_fresh_repo(context.show_db_path)
|
|
with patch(_PATCH_PROJECT_REPO, return_value=repo):
|
|
context.show_result = runner.invoke(project_app, ["show", name])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then('the project-show output should contain "{text}"')
|
|
def step_show_output_contains(context: Any, text: str) -> None:
|
|
"""Assert that the show output contains *text* (case-insensitive)."""
|
|
result = context.show_result
|
|
assert result is not None, "project show was not invoked"
|
|
output = result.output
|
|
assert text.lower() in output.lower(), (
|
|
f"Expected '{text}' in show output but got:\n{output}"
|
|
)
|
|
|
|
|
|
@then("the project-show exit code should be {code:d}")
|
|
def step_show_exit_code(context: Any, code: int) -> None:
|
|
"""Assert the show command exited with the expected code."""
|
|
result = context.show_result
|
|
assert result is not None, "project show was not invoked"
|
|
actual = result.exit_code
|
|
assert actual == code, (
|
|
f"Expected exit code {code}, got {actual}. Output:\n{result.output}"
|
|
)
|
|
|
|
|
|
@then("the project-show exit code should not be {code:d}")
|
|
def step_show_exit_code_not(context: Any, code: int) -> None:
|
|
"""Assert the show command did NOT exit with the given code."""
|
|
result = context.show_result
|
|
assert result is not None, "project show was not invoked"
|
|
actual = result.exit_code
|
|
assert actual != code, (
|
|
f"Expected exit code != {code}, got {actual}. Output:\n{result.output}"
|
|
)
|