diff --git a/lib/crewai/src/crewai/flow/persistence/decorators.py b/lib/crewai/src/crewai/flow/persistence/decorators.py index 48b917760e..635c8a5858 100644 --- a/lib/crewai/src/crewai/flow/persistence/decorators.py +++ b/lib/crewai/src/crewai/flow/persistence/decorators.py @@ -53,11 +53,16 @@ async def async_method(self): } +class _MissingFlowStateError(ValueError): + """Signal that a flow has no state without conflating validation errors.""" + + def _stamp_persistence_metadata( target: Any, persistence: FlowPersistence, verbose: bool, ) -> None: + """Attach persistence configuration metadata to a flow target.""" target.__flow_persistence_config__ = SimpleNamespace( persistence=persistence, verbose=verbose, @@ -89,12 +94,11 @@ def persist_state( Raises: ValueError: If flow has no state or state lacks an ID RuntimeError: If state persistence fails - AttributeError: If flow instance lacks required state attributes """ try: state = getattr(flow_instance, "state", None) if state is None: - raise ValueError("Flow instance has no state") + raise _MissingFlowStateError flow_uuid: str | None = None if isinstance(state, dict): @@ -130,7 +134,7 @@ def persist_state( PRINTER.print(error_msg, color="red") logger.error(error_msg) raise RuntimeError(f"State persistence failed: {e!s}") from e - except AttributeError as e: + except (_MissingFlowStateError, AttributeError) as e: error_msg = LOG_MESSAGES["state_missing"] if verbose: PRINTER.print(error_msg, color="red") diff --git a/lib/crewai/tests/test_flow_persistence.py b/lib/crewai/tests/test_flow_persistence.py index b405cc64d3..6cc3b0980a 100644 --- a/lib/crewai/tests/test_flow_persistence.py +++ b/lib/crewai/tests/test_flow_persistence.py @@ -1,11 +1,13 @@ """Test flow state persistence functionality.""" import os +from types import SimpleNamespace from typing import Dict, List import pytest from crewai.flow.flow import Flow, FlowState, listen, start from crewai.flow.persistence import persist +from crewai.flow.persistence.decorators import PersistenceDecorator from crewai.flow.persistence.sqlite import SQLiteFlowPersistence from pydantic import BaseModel @@ -17,6 +19,33 @@ class TestState(FlowState): message: str = "" +@pytest.mark.parametrize( + ("flow_instance", "expected_message"), + [ + (SimpleNamespace(), "Flow instance has no state"), + (SimpleNamespace(state=None), "Flow instance has no state"), + ( + SimpleNamespace(state={}), + "Flow state must have an 'id' field for persistence", + ), + ], +) +def test_persist_state_reports_specific_validation_error( + tmp_path, flow_instance, expected_message +): + """Report whether the state itself or only its ID is missing.""" + persistence = SQLiteFlowPersistence(str(tmp_path / "test_flows.db")) + + with pytest.raises(ValueError) as exc_info: + PersistenceDecorator.persist_state( + flow_instance, + "test_method", + persistence, + ) + + assert str(exc_info.value) == expected_message + + def test_persist_decorator_saves_state(tmp_path, caplog): """Test that @persist decorator saves state in SQLite.""" db_path = os.path.join(tmp_path, "test_flows.db")