diff --git a/features/llm_trace.feature b/features/llm_trace.feature index feb827178..dc6d58a46 100644 --- a/features/llm_trace.feature +++ b/features/llm_trace.feature @@ -269,8 +269,20 @@ Feature: LLM trace observability Given LANGCHAIN_TRACING_V2 is set to "TRUE" Then the langsmith_enabled check should return True - Scenario: Repository save trace with no tool calls stores null - Given a SQLAlchemy in-memory repository - And a valid LLM trace - When I save the trace via the repository - Then the raw tool_calls_json in the database should be null + Scenario: Repository save trace with no tool calls stores null + Given a SQLAlchemy in-memory repository + And a valid LLM trace + When I save the trace via the repository + Then the raw tool_calls_json in the database should be null + + # --- UnitOfWork transaction boundary (Issue #7505) ---------------------- + + @tdd_issue_7505 + Scenario: Repository save uses flush not commit within UnitOfWork + Given a SQLAlchemy in-memory repository + And a valid LLM trace + When I save the trace via the repository within a UnitOfWork transaction + Then the trace should be visible within the transaction + And the transaction should not be committed yet + And I should be able to rollback the transaction + And the trace should not exist after rollback diff --git a/features/steps/llm_trace_steps.py b/features/steps/llm_trace_steps.py index dbd635e52..ac1ec1434 100644 --- a/features/steps/llm_trace_steps.py +++ b/features/steps/llm_trace_steps.py @@ -946,3 +946,63 @@ def step_raw_tool_calls_null(context: Context) -> None: ) assert row is not None assert row.tool_calls_json is None + + +# --------------------------------------------------------------------------- +# UnitOfWork transaction boundary (Issue #7505) +# --------------------------------------------------------------------------- + + +@when("I save the trace via the repository within a UnitOfWork transaction") +def step_save_within_uow(context: Context) -> None: + """Save a trace within a UnitOfWork transaction to verify flush behavior.""" + # Create a new session for the UnitOfWork transaction + session = context.sqla_factory() + context.uow_session = session + + # Save the trace using the SQLAlchemy repository + # The repository should use flush(), not commit() + context.sqla_repo.save(context.trace) + + # At this point, the trace should be visible within the transaction + # but not yet committed to the database + + +@then("the trace should be visible within the transaction") +def step_trace_visible_in_transaction(context: Context) -> None: + """Verify the trace is visible within the UnitOfWork transaction.""" + session = context.uow_session + row = ( + session.query(LLMTraceModel).filter_by(trace_id=context.trace.trace_id).first() + ) + assert row is not None, "Trace should be visible within the transaction" + + +@then("the transaction should not be committed yet") +def step_transaction_not_committed(context: Context) -> None: + """Verify the transaction is still open and not committed.""" + session = context.uow_session + # Check that the session is still in a transaction + assert session.in_transaction(), "Session should still be in a transaction" + + +@then("I should be able to rollback the transaction") +def step_rollback_transaction(context: Context) -> None: + """Rollback the UnitOfWork transaction.""" + session = context.uow_session + session.rollback() + context.transaction_rolled_back = True + + +@then("the trace should not exist after rollback") +def step_trace_not_exist_after_rollback(context: Context) -> None: + """Verify the trace was rolled back and no longer exists.""" + # Create a new session to check the database state + new_session = context.sqla_factory() + row = ( + new_session.query(LLMTraceModel) + .filter_by(trace_id=context.trace.trace_id) + .first() + ) + assert row is None, "Trace should not exist after rollback" + new_session.close() diff --git a/src/cleveragents/infrastructure/database/llm_trace_repository.py b/src/cleveragents/infrastructure/database/llm_trace_repository.py index bf6c28e10..70fcfa1ae 100644 --- a/src/cleveragents/infrastructure/database/llm_trace_repository.py +++ b/src/cleveragents/infrastructure/database/llm_trace_repository.py @@ -78,7 +78,7 @@ class LLMTraceRepository: timestamp=trace.timestamp.isoformat(), ) session.add(model) - session.commit() + session.flush() except (SQLAlchemyDatabaseError, OperationalError) as exc: session.rollback() raise DatabaseError(f"Failed to save LLM trace: {exc}") from exc