Files
temp/features/steps/project_show_after_create_steps.py
brent.edwards 16dc4ab18d fix(project): show created project after creation (#593)
## 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>
2026-03-09 07:56:38 +00:00

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