diff --git a/src/aws_durable_execution_sdk_python_testing/executor.py b/src/aws_durable_execution_sdk_python_testing/executor.py index 2b2fbe7..02a1504 100644 --- a/src/aws_durable_execution_sdk_python_testing/executor.py +++ b/src/aws_durable_execution_sdk_python_testing/executor.py @@ -323,14 +323,14 @@ def stop_execution( Raises: ResourceNotFoundException: If execution does not exist - ExecutionAlreadyStartedException: If execution is already completed """ execution = self.get_execution(execution_arn) if execution.is_complete: - # Context-aware mapping: execution already completed maps to ExecutionAlreadyStartedException - msg: str = f"Execution {execution_arn} is already completed" - raise ExecutionAlreadyStartedException(msg, execution_arn) + # Idempotent: return the existing stop timestamp + execution_op = execution.get_operation_execution_started() + stop_timestamp = execution_op.end_timestamp or datetime.now(UTC) + return StopDurableExecutionResponse(stop_timestamp=stop_timestamp) # Use provided error or create a default one stop_error = error or ErrorObject.from_message( diff --git a/tests/executor_test.py b/tests/executor_test.py index 8e50f2a..e228a0d 100644 --- a/tests/executor_test.py +++ b/tests/executor_test.py @@ -41,6 +41,7 @@ SendDurableExecutionCallbackHeartbeatResponse, SendDurableExecutionCallbackSuccessResponse, StartDurableExecutionInput, + StopDurableExecutionResponse, ) from aws_durable_execution_sdk_python_testing.observer import ( ExecutionNotifier, @@ -2056,13 +2057,22 @@ def test_stop_execution(executor, mock_store): def test_stop_execution_already_complete(executor, mock_store): - """Test stop_execution with already completed execution.""" + """Test stop_execution with already completed execution returns idempotent response.""" mock_execution = Mock() mock_execution.is_complete = True + mock_execution.durable_execution_arn = "test-arn" + + # Mock the execution operation with end_timestamp + mock_execution_op = Mock() + mock_execution_op.end_timestamp = datetime(2023, 1, 1, 0, 1, 0, tzinfo=UTC) + mock_execution.get_operation_execution_started.return_value = mock_execution_op + mock_store.load.return_value = mock_execution - with pytest.raises(ExecutionAlreadyStartedException, match="already completed"): - executor.stop_execution("test-arn") + result = executor.stop_execution("test-arn") + + assert isinstance(result, StopDurableExecutionResponse) + assert result.stop_timestamp == datetime(2023, 1, 1, 0, 1, 0, tzinfo=UTC) def test_stop_execution_with_custom_error(executor, mock_store): diff --git a/tests/web/handlers_test.py b/tests/web/handlers_test.py index 167b0f5..d08932b 100644 --- a/tests/web/handlers_test.py +++ b/tests/web/handlers_test.py @@ -911,14 +911,15 @@ def test_stop_durable_execution_handler_success(): def test_stop_durable_execution_handler_execution_already_stopped(): - """Test StopDurableExecutionHandler with execution already stopped error.""" + """Test StopDurableExecutionHandler with execution already stopped returns idempotent response.""" executor = Mock() handler = StopDurableExecutionHandler(executor) - # Mock executor to raise IllegalStateException - executor.stop_execution.side_effect = IllegalStateException( - "Execution test-arn is already completed" + # Mock executor to return stop response with timestamp + stop_timestamp = "2023-01-01T00:01:00Z" + executor.stop_execution.return_value = StopDurableExecutionResponse( + stop_timestamp=stop_timestamp ) request_body = { @@ -941,10 +942,9 @@ def test_stop_durable_execution_handler_execution_already_stopped(): response = handler.handle(typed_route, request) - # Verify IllegalStateException maps to ServiceException in AWS-compliant format - assert response.status_code == 500 - assert response.body["Type"] == "ServiceException" - assert response.body["Message"] == "Execution test-arn is already completed" + # Verify idempotent response with stop timestamp + assert response.status_code == 200 + assert response.body["StopTimestamp"] == stop_timestamp def test_stop_durable_execution_handler_resource_not_found():