fix(project): persist project to database during creation #591

Merged
hurui200320 merged 1 commits from feature/m3-fix-project-create-persist into master 2026-03-09 06:59:16 +00:00
7 changed files with 404 additions and 5 deletions
+6
View File
@@ -2,6 +2,12 @@
## Unreleased
- Fixed `agents project create` not persisting projects to the database.
`NamespacedProjectRepository.create()` called `session.flush()` but never
`session.commit()`, so projects were invisible to subsequent
`agents project list` calls. Added `session.commit()` and a `finally:
session.close()` guard. Includes 4 Behave BDD regression scenarios,
Robot Framework integration smoke tests, and ASV benchmarks. (#589)
- Added TDD-style Behave BDD tests for the built-in `git-checkout` resource type
bootstrap. Three scenarios: one failing TDD test reproducing bug #524 (no bootstrap
called during init), and two regression tests verifying `bootstrap_builtin_types()`
+110
View File
@@ -0,0 +1,110 @@
"""ASV benchmarks for project create + list persistence round-trip.
Measures the cost of creating a project and then listing it back via the
``NamespacedProjectRepository``, using a file-based SQLite database so
that each operation gets a fresh session (matching real CLI behaviour).
Targets bug #589 — ``create()`` now calls ``session.commit()`` so data
is visible to a subsequent ``list()`` in a separate session.
"""
from __future__ import annotations
import importlib
import os
import shutil
import sys
import tempfile
from pathlib import Path
# Ensure the local *source* tree is importable even when ASV has an
# older build of the package installed.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
Outdated
Review

F4 -- MEDIUM | Convention: This try/except ModuleNotFoundError import pattern deviates from the established convention used across the other 166 benchmark files in this repo (e.g., acms_backends_bench.py, action_cli_bench.py). The standard pattern is:

_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
    sys.path.insert(0, _SRC)
import cleveragents
importlib.reload(cleveragents)

The try/except approach has two practical downsides:

  1. Skips importlib.reload() -- ASV may load a stale installed package instead of the source tree.
  2. Masks non-ModuleNotFoundError failures -- a renamed class or circular import would trigger the except branch, insert the path, and attempt the import again with a confusing double traceback.

Recommendation: Align with the codebase convention.

**F4 -- MEDIUM | Convention**: This try/except `ModuleNotFoundError` import pattern deviates from the established convention used across the other 166 benchmark files in this repo (e.g., `acms_backends_bench.py`, `action_cli_bench.py`). The standard pattern is: ```python _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) import cleveragents importlib.reload(cleveragents) ``` The try/except approach has two practical downsides: 1. **Skips `importlib.reload()`** -- ASV may load a stale installed package instead of the source tree. 2. **Masks non-`ModuleNotFoundError` failures** -- a renamed class or circular import would trigger the `except` branch, insert the path, and attempt the import again with a confusing double traceback. **Recommendation:** Align with the codebase convention.
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Force-reload so ASV picks up the source tree version.
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from sqlalchemy import create_engine # noqa: E402
from sqlalchemy.orm import sessionmaker # noqa: E402
from sqlalchemy.pool import NullPool # noqa: E402
from cleveragents.domain.models.core.project import ( # noqa: E402
NamespacedProject,
parse_namespaced_name,
)
from cleveragents.infrastructure.database.models import Base # noqa: E402
from cleveragents.infrastructure.database.repositories import ( # noqa: E402
NamespacedProjectRepository,
)
def _fresh_repo(db_path: str) -> NamespacedProjectRepository:
"""Return a repo backed by a *new* engine/session for *db_path*.
``NullPool`` ensures SQLite file handles are released immediately
when the engine is no longer referenced.
Outdated
Review

F6 -- MEDIUM | Performance: Same engine leak issue as the Behave steps. create_engine() is called but never engine.dispose()-d. Consider using poolclass=NullPool or adding explicit engine.dispose() after the repo is used.

**F6 -- MEDIUM | Performance**: Same engine leak issue as the Behave steps. `create_engine()` is called but never `engine.dispose()`-d. Consider using `poolclass=NullPool` or adding explicit `engine.dispose()` after the repo is used.
"""
engine = create_engine(f"sqlite:///{db_path}", echo=False, poolclass=NullPool)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
return NamespacedProjectRepository(session_factory=factory)
class ProjectPersistRoundTripSuite:
"""Benchmark create-then-list round trip with separate sessions."""
timeout = 30.0
def setup(self) -> None:
self._tmpdir = tempfile.mkdtemp(prefix="bench_persist_589_")
self._db_path = os.path.join(self._tmpdir, "bench.db")
# Initialise schema once.
engine = create_engine(
f"sqlite:///{self._db_path}",
echo=False,
poolclass=NullPool,
)
Base.metadata.create_all(engine)
engine.dispose()
self._counter = 0
def teardown(self) -> None:
shutil.rmtree(self._tmpdir, ignore_errors=True)
def time_create_then_list(self) -> None:
"""Create a project in one session, list in another."""
self._counter += 1
name = f"bench-{self._counter}"
parsed = parse_namespaced_name(name)
proj = NamespacedProject(name=parsed.name, namespace=parsed.namespace)
create_repo = _fresh_repo(self._db_path)
create_repo.create(proj)
Outdated
Review

F5 -- MEDIUM | Bug Detection: This method always creates a project named "bench-track". If ASV runs it more than once per setup() invocation (which can happen under certain ASV configurations or when comparing across commits), the second call will hit a UNIQUE constraint violation once the persistence fix is applied, crashing the benchmark.

Compare to time_create_then_list (line 74) which correctly uses self._counter to generate unique names per invocation.

Recommendation: Use self._counter or uuid4() for the project name:

self._counter += 1
parsed = parse_namespaced_name(f"bench-track-{self._counter}")
**F5 -- MEDIUM | Bug Detection**: This method always creates a project named `"bench-track"`. If ASV runs it more than once per `setup()` invocation (which can happen under certain ASV configurations or when comparing across commits), the second call will hit a UNIQUE constraint violation once the persistence fix is applied, crashing the benchmark. Compare to `time_create_then_list` (line 74) which correctly uses `self._counter` to generate unique names per invocation. **Recommendation:** Use `self._counter` or `uuid4()` for the project name: ```python self._counter += 1 parsed = parse_namespaced_name(f"bench-track-{self._counter}") ```
list_repo = _fresh_repo(self._db_path)
list_repo.list_projects()
def track_list_after_create_count(self) -> int:
"""Track whether list sees the created project.
Returns the number of projects visible after create + commit.
Expected value is 1 (or more if called repeatedly).
"""
self._counter += 1
name = f"bench-track-{self._counter}"
parsed = parse_namespaced_name(name)
proj = NamespacedProject(name=parsed.name, namespace=parsed.namespace)
create_repo = _fresh_repo(self._db_path)
create_repo.create(proj)
list_repo = _fresh_repo(self._db_path)
projects = list_repo.list_projects()
return len(projects)
ProjectPersistRoundTripSuite.track_list_after_create_count.unit = "projects" # type: ignore[attr-defined]
+34
View File
@@ -0,0 +1,34 @@
# Regression tests for bug #589 — project create must commit to the database.
Feature: Project create persists to database
As a developer using the agents CLI
I want projects created with "agents project create" to persist in the database
So that "agents project list" shows previously created projects
Background:
Given a fresh project-persist database is initialised
@tdd @bug589
Scenario: Created project appears in project list
Outdated
Review

F9 -- LOW | Coverage: All three scenarios use fully-qualified names (local/my-app, local/alpha, local/beta). The issue's own reproduction steps use a bare name (agents project create project1) which relies on parse_namespaced_name defaulting to the local/ namespace. Adding one scenario with a bare name (e.g., project1 instead of local/project1) would more faithfully reproduce the reported bug and exercise the default-namespace resolution path end-to-end.

This is minor since parse_namespaced_name has its own test suite, but worth noting for completeness.

**F9 -- LOW | Coverage**: All three scenarios use fully-qualified names (`local/my-app`, `local/alpha`, `local/beta`). The issue's own reproduction steps use a **bare name** (`agents project create project1`) which relies on `parse_namespaced_name` defaulting to the `local/` namespace. Adding one scenario with a bare name (e.g., `project1` instead of `local/project1`) would more faithfully reproduce the reported bug and exercise the default-namespace resolution path end-to-end. This is minor since `parse_namespaced_name` has its own test suite, but worth noting for completeness.
When I create a project named "local/my-app" via the persist CLI
And I list projects via the persist CLI
Then the persist project list should contain "local/my-app"
@tdd @bug589
Scenario: Multiple created projects all appear in list
When I create a project named "local/alpha" via the persist CLI
And I create a project named "local/beta" via the persist CLI
And I list projects via the persist CLI
Then the persist project list should contain "local/alpha"
And the persist project list should contain "local/beta"
@tdd @bug589
Scenario: Bare project name uses default namespace and persists
When I create a project named "my-app" via the persist CLI
And I list projects via the persist CLI
Then the persist project list should contain "local/my-app"
@tdd @bug589
Scenario: Creating a duplicate project produces an error
When I create a project named "local/dup-proj" via the persist CLI
And I attempt to create a duplicate project named "local/dup-proj" via the persist CLI
Then the persist duplicate creation should fail with "already exists"
@@ -0,0 +1,184 @@
"""Step definitions for project_create_persist.feature (bug #589).
Regression tests verifying that ``agents project create`` commits data
to the database so that ``agents project list`` shows previously created
projects.
Design rationale
~~~~~~~~~~~~~~~~
The bug was that ``NamespacedProjectRepository.create()`` called
``session.flush()`` but never ``session.commit()``. Each CLI invocation
obtains a **new** session from the factory, so flushed-but-uncommitted
rows were invisible to subsequent invocations.
To exercise the fix we use a **file-based** SQLite database and patch
``_get_namespaced_project_repo`` so that every CLI invocation receives a
*fresh* ``NamespacedProjectRepository`` backed by a **new session
factory** pointing at the same file. This mirrors real CLI behaviour
where each ``agents project …`` invocation gets its own session.
"""
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 target - the module-level DI helper that returns the project repo.
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
# The --invariant flag is never passed in these scenarios, so
# _store_project_extras is effectively a no-op. We patch it defensively
# to avoid side-effects from DI look-ups in the real implementation.
_PATCH_STORE_EXTRAS = "cleveragents.cli.commands.project._store_project_extras"
runner = CliRunner()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_fresh_repo(db_path: str) -> NamespacedProjectRepository:
"""Build a *new* repo with its own engine/session for *db_path*.
Each call simulates a separate CLI invocation: a new engine and session
factory are created, so uncommitted data from a prior invocation is not
visible. ``NullPool`` is used so that SQLite file handles are released
immediately when the engine is no longer referenced.
Schema is pre-created once during the Background step, so we skip
``Base.metadata.create_all()`` here to avoid redundant DDL.
Outdated
Review

F6 -- MEDIUM | Performance: create_engine() is called here but the engine is never dispose()-d. Each call opens a connection pool to the SQLite file. Over a test suite run with multiple scenarios, this accumulates unclosed connections and file handles. With SQLite specifically, this can cause file-lock contention when _cleanup_persist_db attempts to shutil.rmtree() the temp directory (dangling connections hold the file open).

Recommendation: Either store the engine on the context for disposal in _cleanup_persist_db, or use poolclass=NullPool (from sqlalchemy.pool) so connections are not pooled:

from sqlalchemy.pool import NullPool
engine = create_engine(f"sqlite:///{db_path}", echo=False, poolclass=NullPool)
**F6 -- MEDIUM | Performance**: `create_engine()` is called here but the engine is never `dispose()`-d. Each call opens a connection pool to the SQLite file. Over a test suite run with multiple scenarios, this accumulates unclosed connections and file handles. With SQLite specifically, this can cause file-lock contention when `_cleanup_persist_db` attempts to `shutil.rmtree()` the temp directory (dangling connections hold the file open). **Recommendation:** Either store the engine on the context for disposal in `_cleanup_persist_db`, or use `poolclass=NullPool` (from `sqlalchemy.pool`) so connections are not pooled: ```python from sqlalchemy.pool import NullPool engine = create_engine(f"sqlite:///{db_path}", echo=False, poolclass=NullPool) ```
"""
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_persist_db(context: Any) -> None:
"""Remove the temporary directory created for the test."""
shutil.rmtree(context.persist_tmpdir, ignore_errors=True)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a fresh project-persist database is initialised")
def step_init_persist_db(context: Any) -> None:
"""Create a temp dir with a fresh SQLite file for persistence tests."""
context.persist_tmpdir = tempfile.mkdtemp(prefix="persist_589_")
context.persist_db_path = os.path.join(context.persist_tmpdir, "test.db")
# Pre-create schema so every fresh repo sees the tables.
engine = create_engine(
f"sqlite:///{context.persist_db_path}",
echo=False,
poolclass=NullPool,
)
Base.metadata.create_all(engine)
engine.dispose()
# Initialise result holders.
context.persist_create_results = [] # type: ignore[attr-defined]
context.persist_list_result = None
context.persist_dup_result = None
context.add_cleanup(_cleanup_persist_db, context)
# ---------------------------------------------------------------------------
Outdated
Review

F10 -- LOW | Convention: _store_project_extras is only called in the create command when --invariant or --invariant-actor flags are passed. Since the tests pass neither flag, this function is never invoked and the patch is unnecessary. While harmless, it adds noise about test intent and could silently mask future bugs if the create command's control flow is refactored to always call this function.

Recommendation: Remove the patch(_PATCH_STORE_EXTRAS) context manager, or add a comment explaining it's defensive.

**F10 -- LOW | Convention**: `_store_project_extras` is only called in the `create` command when `--invariant` or `--invariant-actor` flags are passed. Since the tests pass neither flag, this function is never invoked and the patch is unnecessary. While harmless, it adds noise about test intent and could silently mask future bugs if the `create` command's control flow is refactored to always call this function. **Recommendation:** Remove the `patch(_PATCH_STORE_EXTRAS)` context manager, or add a comment explaining it's defensive.
# When steps - create
# ---------------------------------------------------------------------------
Outdated
Review

F3 -- MEDIUM | Test Flaws: The result is appended to context.persist_create_results but no scenario step ever asserts result.exit_code == 0. If the CLI create fails (DI container error, import error), the test silently continues to the list step, which then fails with a confusing assertion like "Expected 'local/my-app' in project list output" rather than identifying the create failure.

Recommendation: Either add an inline assertion here:

assert result.exit_code == 0, (
    f"project create failed (exit {result.exit_code}): {result.output}"
)

or add a new Gherkin Then step like Then the persist creation should succeed that checks context.persist_create_results[-1].exit_code.

**F3 -- MEDIUM | Test Flaws**: The result is appended to `context.persist_create_results` but **no scenario step ever asserts `result.exit_code == 0`**. If the CLI create fails (DI container error, import error), the test silently continues to the list step, which then fails with a confusing assertion like _"Expected 'local/my-app' in project list output"_ rather than identifying the create failure. **Recommendation:** Either add an inline assertion here: ```python assert result.exit_code == 0, ( f"project create failed (exit {result.exit_code}): {result.output}" ) ``` or add a new Gherkin `Then` step like `Then the persist creation should succeed` that checks `context.persist_create_results[-1].exit_code`.
@when('I create a project named "{name}" via the persist CLI')
def step_create_project_persist(context: Any, name: str) -> None:
"""Invoke ``project create <name>`` with a fresh repo per call."""
repo = _make_fresh_repo(context.persist_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}"
)
context.persist_create_results.append(result)
@when('I attempt to create a duplicate project named "{name}" via the persist CLI')
def step_create_duplicate_persist(context: Any, name: str) -> None:
"""Invoke ``project create`` again for the same name (expect error)."""
repo = _make_fresh_repo(context.persist_db_path)
with (
patch(_PATCH_PROJECT_REPO, return_value=repo),
patch(_PATCH_STORE_EXTRAS),
):
result = runner.invoke(project_app, ["create", name])
context.persist_dup_result = result
# ---------------------------------------------------------------------------
# When steps - list
# ---------------------------------------------------------------------------
@when("I list projects via the persist CLI")
def step_list_projects_persist(context: Any) -> None:
"""Invoke ``project list`` with a fresh repo pointing at the same DB."""
repo = _make_fresh_repo(context.persist_db_path)
with patch(_PATCH_PROJECT_REPO, return_value=repo):
result = runner.invoke(project_app, ["list"])
assert result.exit_code == 0, (
f"project list should exit 0 but got {result.exit_code}. "
f"output:\n{result.output}"
)
context.persist_list_result = result
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then('the persist project list should contain "{name}"')
def step_persist_list_contains(context: Any, name: str) -> None:
"""Assert that the list output includes the given project name."""
result = context.persist_list_result
assert result is not None, "project list was not invoked"
output = result.output
assert name in output, (
f"Expected '{name}' in project list output but got:\n{output}"
)
@then('the persist duplicate creation should fail with "{text}"')
def step_persist_dup_fails(context: Any, text: str) -> None:
"""Assert that the duplicate creation produced an error containing *text*."""
result = context.persist_dup_result
assert result is not None, "duplicate create was not invoked"
assert result.exit_code != 0, (
f"Duplicate create should fail (exit_code != 0) but got "
f"{result.exit_code}. output:\n{result.output}"
)
output = result.output
assert text.lower() in output.lower(), (
f"Expected '{text}' in duplicate-create output but got:\n{output}"
)
@@ -61,10 +61,11 @@ def _mock_session_op_error_on_query() -> MagicMock:
def _mock_session_op_error_on_flush() -> MagicMock:
"""Session whose .flush() raises OperationalError but .add()/.query() work."""
"""Session whose .flush()/.commit() raises OperationalError but .add()/.query() work."""
mock = MagicMock()
mock.add.return_value = None
mock.flush.side_effect = OperationalError("disk I/O error", {}, None)
mock.commit.side_effect = OperationalError("disk I/O error", {}, None)
mock.rollback.return_value = None
# query returns a mock that allows filter_by().first() to return None
query_mock = MagicMock()
@@ -75,12 +76,15 @@ def _mock_session_op_error_on_flush() -> MagicMock:
def _mock_session_unique_integrity_on_flush() -> MagicMock:
"""Session whose .flush() raises UNIQUE IntegrityError."""
"""Session whose .flush()/.commit() raises UNIQUE IntegrityError."""
mock = MagicMock()
mock.add.return_value = None
mock.flush.side_effect = IntegrityError(
"UNIQUE constraint failed: table.name", {}, None
)
mock.commit.side_effect = IntegrityError(
"UNIQUE constraint failed: table.name", {}, None
)
mock.rollback.return_value = None
query_mock = MagicMock()
query_mock.filter_by.return_value.first.return_value = None
@@ -90,12 +94,15 @@ def _mock_session_unique_integrity_on_flush() -> MagicMock:
def _mock_session_non_unique_integrity_on_flush() -> MagicMock:
"""Session whose .flush() raises a non-UNIQUE IntegrityError."""
"""Session whose .flush()/.commit() raises a non-UNIQUE IntegrityError."""
mock = MagicMock()
mock.add.return_value = None
mock.flush.side_effect = IntegrityError(
"CHECK constraint failed: some_check", {}, None
)
mock.commit.side_effect = IntegrityError(
"CHECK constraint failed: some_check", {}, None
)
mock.rollback.return_value = None
query_mock = MagicMock()
query_mock.filter_by.return_value.first.return_value = None
@@ -105,10 +112,11 @@ def _mock_session_non_unique_integrity_on_flush() -> MagicMock:
def _mock_session_integrity_on_flush() -> MagicMock:
"""Session whose .flush() raises IntegrityError (generic)."""
"""Session whose .flush()/.commit() raises IntegrityError (generic)."""
mock = MagicMock()
mock.add.return_value = None
mock.flush.side_effect = IntegrityError("FOREIGN KEY constraint failed", {}, None)
mock.commit.side_effect = IntegrityError("FOREIGN KEY constraint failed", {}, None)
mock.rollback.return_value = None
query_mock = MagicMock()
query_mock.filter_by.return_value.first.return_value = None
+55
View File
@@ -0,0 +1,55 @@
*** Settings ***
Documentation Integration smoke test for project create persistence (bug #589).
... Verifies that ``agents project create`` commits to the database
... so that ``agents project list`` shows previously created projects.
Resource ${CURDIR}/common.resource
Library Process
Library OperatingSystem
Library String
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Test Cases ***
Project Create Then List Shows Created Project
[Documentation] After creating a project, listing projects should include it.
${tmpdir}= Evaluate __import__('tempfile').mkdtemp(prefix='persist_589_')
${init}= Run Process ${PYTHON} -m cleveragents init persist-test
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${init.rc} 0
... msg=agents init should exit 0 but got ${init.rc}. stderr: ${init.stderr}
Outdated
Review

F2 -- HIGH | Test Flaws: agents init is not captured into a variable in either test case (here and line 37). Its exit code and stderr are never verified. If initialization fails, all downstream operations fail with misleading assertion errors instead of a clear "init failed" signal.

Recommendation: Change to ${init}= Run Process ... and add Should Be Equal As Integers ${init.rc} 0 msg=agents init should exit 0 but got ${init.rc}. stderr: ${init.stderr} in both test cases.

**F2 -- HIGH | Test Flaws**: `agents init` is not captured into a variable in either test case (here and line 37). Its exit code and stderr are never verified. If initialization fails, all downstream operations fail with misleading assertion errors instead of a clear "init failed" signal. **Recommendation:** Change to `${init}= Run Process ...` and add `Should Be Equal As Integers ${init.rc} 0 msg=agents init should exit 0 but got ${init.rc}. stderr: ${init.stderr}` in both test cases.
${create}= Run Process ${PYTHON} -m cleveragents project create local/smoke-proj
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${create.rc} 0
... msg=project create should exit 0 but got ${create.rc}. stderr: ${create.stderr}
${list}= Run Process ${PYTHON} -m cleveragents project list
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${list.rc} 0
... msg=project list should exit 0 but got ${list.rc}. stderr: ${list.stderr}
Should Contain ${list.stdout} local/smoke-proj
... msg=project list output should contain 'local/smoke-proj' but got: ${list.stdout}
[Teardown] Remove Directory ${tmpdir} recursive=True
Multiple Created Projects Appear In List
[Documentation] Creating two projects should result in both appearing in list.
${tmpdir}= Evaluate __import__('tempfile').mkdtemp(prefix='persist_589_multi_')
${init}= Run Process ${PYTHON} -m cleveragents init persist-multi
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${init.rc} 0
Outdated
Review

F1 -- HIGH | Test Flaws: This entire second test case fires three commands (init, create local/first, create local/second) without capturing their process result or checking exit codes. Compare to the first test case (lines 19-24) which properly captures ${create} and asserts ${create.rc} == 0.

If agents init fails (permissions, disk, migration error), both creates silently fail, and the final Should Contain on the list output produces a misleading failure about missing project names instead of surfacing the real root cause.

Recommendation: Capture all three process results and add Should Be Equal As Integers ${result.rc} 0 after each, mirroring the pattern already used in the first test case.

**F1 -- HIGH | Test Flaws**: This entire second test case fires three commands (`init`, `create local/first`, `create local/second`) without capturing their process result or checking exit codes. Compare to the first test case (lines 19-24) which properly captures `${create}` and asserts `${create.rc} == 0`. If `agents init` fails (permissions, disk, migration error), both creates silently fail, and the final `Should Contain` on the list output produces a misleading failure about missing project names instead of surfacing the real root cause. **Recommendation:** Capture all three process results and add `Should Be Equal As Integers ${result.rc} 0` after each, mirroring the pattern already used in the first test case.
... msg=agents init should exit 0 but got ${init.rc}. stderr: ${init.stderr}
${create_first}= Run Process ${PYTHON} -m cleveragents project create local/first
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${create_first.rc} 0
... msg=project create local/first should exit 0 but got ${create_first.rc}. stderr: ${create_first.stderr}
${create_second}= Run Process ${PYTHON} -m cleveragents project create local/second
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${create_second.rc} 0
Outdated
Review

F7 -- LOW | Test Flaws: Unlike the first test (line 27) which asserts Should Be Equal As Integers ${list.rc} 0, this second test jumps directly to Should Contain on stdout. If project list exits non-zero, ${list.stdout} could be empty while the actual error is in ${list.stderr}.

Recommendation: Add Should Be Equal As Integers ${list.rc} 0 before the Should Contain assertions, consistent with the first test case.

**F7 -- LOW | Test Flaws**: Unlike the first test (line 27) which asserts `Should Be Equal As Integers ${list.rc} 0`, this second test jumps directly to `Should Contain` on stdout. If `project list` exits non-zero, `${list.stdout}` could be empty while the actual error is in `${list.stderr}`. **Recommendation:** Add `Should Be Equal As Integers ${list.rc} 0` before the `Should Contain` assertions, consistent with the first test case.
... msg=project create local/second should exit 0 but got ${create_second.rc}. stderr: ${create_second.stderr}
${list}= Run Process ${PYTHON} -m cleveragents project list
... timeout=60s cwd=${tmpdir}
Should Be Equal As Integers ${list.rc} 0
... msg=project list should exit 0 but got ${list.rc}. stderr: ${list.stderr}
Should Contain ${list.stdout} local/first
... msg=project list should contain 'local/first' but got: ${list.stdout}
Should Contain ${list.stdout} local/second
... msg=project list should contain 'local/second' but got: ${list.stdout}
[Teardown] Remove Directory ${tmpdir} recursive=True
@@ -2841,7 +2841,7 @@ class NamespacedProjectRepository:
try:
db_model = NamespacedProjectModel.from_domain(project)
session.add(db_model)
session.flush()
session.commit()
return project
except IntegrityError as exc:
session.rollback()
@@ -2853,6 +2853,8 @@ class NamespacedProjectRepository:
except (OperationalError, SQLAlchemyDatabaseError) as exc:
session.rollback()
raise DatabaseError(f"Failed to create project: {exc}") from exc
finally:
session.close()
@database_retry
def get(self, namespaced_name: str) -> Any: