diff --git a/src/aws_durable_execution_sdk_python_testing/execution.py b/src/aws_durable_execution_sdk_python_testing/execution.py index 113db5b..c6be55f 100644 --- a/src/aws_durable_execution_sdk_python_testing/execution.py +++ b/src/aws_durable_execution_sdk_python_testing/execution.py @@ -28,6 +28,7 @@ # Import AWS exceptions from aws_durable_execution_sdk_python_testing.model import ( + InvocationCompletedDetails, StartDurableExecutionInput, ) from aws_durable_execution_sdk_python_testing.token import ( @@ -60,6 +61,7 @@ def __init__( self.start_input: StartDurableExecutionInput = start_input self.operations: list[Operation] = operations self.updates: list[OperationUpdate] = [] + self.invocation_completions: list[InvocationCompletedDetails] = [] self.used_tokens: set[str] = set() # TODO: this will need to persist/rehydrate depending on inmemory vs sqllite store self._token_sequence: int = 0 @@ -101,6 +103,9 @@ def to_dict(self) -> dict[str, Any]: "StartInput": self.start_input.to_dict(), "Operations": [op.to_dict() for op in self.operations], "Updates": [update.to_dict() for update in self.updates], + "InvocationCompletions": [ + completion.to_dict() for completion in self.invocation_completions + ], "UsedTokens": list(self.used_tokens), "TokenSequence": self._token_sequence, "IsComplete": self.is_complete, @@ -129,6 +134,10 @@ def from_dict(cls, data: dict[str, Any]) -> Execution: execution.updates = [ OperationUpdate.from_dict(update_data) for update_data in data["Updates"] ] + execution.invocation_completions = [ + InvocationCompletedDetails.from_dict(item) + for item in data.get("InvocationCompletions", []) + ] execution.used_tokens = set(data["UsedTokens"]) execution._token_sequence = data["TokenSequence"] # noqa: SLF001 execution.is_complete = data["IsComplete"] @@ -215,6 +224,18 @@ def has_pending_operations(self, execution: Execution) -> bool: return True return False + def record_invocation_completion( + self, start_timestamp: datetime, end_timestamp: datetime, request_id: str + ) -> None: + """Record an invocation completion event.""" + self.invocation_completions.append( + InvocationCompletedDetails( + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + request_id=request_id, + ) + ) + def complete_success(self, result: str | None) -> None: """Complete execution successfully (DecisionType.COMPLETE_WORKFLOW_EXECUTION).""" self.result = DurableExecutionInvocationOutput( diff --git a/src/aws_durable_execution_sdk_python_testing/executor.py b/src/aws_durable_execution_sdk_python_testing/executor.py index 32abf7d..70bcfa5 100644 --- a/src/aws_durable_execution_sdk_python_testing/executor.py +++ b/src/aws_durable_execution_sdk_python_testing/executor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import time import uuid from datetime import UTC, datetime from typing import TYPE_CHECKING @@ -32,6 +33,8 @@ from aws_durable_execution_sdk_python_testing.model import ( CheckpointDurableExecutionResponse, CheckpointUpdatedExecutionState, + EventCreationContext, + EventType, GetDurableExecutionHistoryResponse, GetDurableExecutionResponse, GetDurableExecutionStateResponse, @@ -44,7 +47,6 @@ StartDurableExecutionOutput, StopDurableExecutionResponse, TERMINAL_STATUSES, - EventCreationContext, ) from aws_durable_execution_sdk_python_testing.model import ( Event as HistoryEvent, @@ -413,6 +415,17 @@ def get_execution_history( updates_dict: dict[str, OperationUpdate] = {u.operation_id: u for u in updates} durable_execution_arn: str = execution.durable_execution_arn + # Add InvocationCompleted events + for completion in execution.invocation_completions: + invocation_event = HistoryEvent.create_invocation_completed( + event_id=0, # Temporary, will be reassigned + event_timestamp=completion.end_timestamp, + start_timestamp=completion.start_timestamp, + end_timestamp=completion.end_timestamp, + request_id=completion.request_id, + ) + all_events.append(invocation_event) + # Generate all events first (without final event IDs) for op in ops: operation_update: OperationUpdate | None = updates_dict.get( @@ -769,14 +782,23 @@ async def invoke() -> None: self._store.save(execution) - response: DurableExecutionInvocationOutput = self._invoker.invoke( + invocation_start = datetime.now(UTC) + invoke_response = self._invoker.invoke( execution.start_input.function_name, invocation_input, execution.start_input.lambda_endpoint, ) + invocation_end = datetime.now(UTC) # Reload execution after invocation in case it was completed via checkpoint execution = self._store.load(execution_arn) + + # Record invocation completion and save immediately + execution.record_invocation_completion( + invocation_start, invocation_end, invoke_response.request_id + ) + self._store.save(execution) + if execution.is_complete: logger.info( "[%s] Execution completed during invocation, ignoring result", @@ -785,6 +807,7 @@ async def invoke() -> None: return # Process successful received response - validate status and handle accordingly + response = invoke_response.invocation_output try: self._validate_invocation_response_and_store( execution_arn, response, execution diff --git a/src/aws_durable_execution_sdk_python_testing/invoker.py b/src/aws_durable_execution_sdk_python_testing/invoker.py index a363340..0549ad7 100644 --- a/src/aws_durable_execution_sdk_python_testing/invoker.py +++ b/src/aws_durable_execution_sdk_python_testing/invoker.py @@ -1,8 +1,10 @@ from __future__ import annotations import json +from dataclasses import dataclass from threading import Lock from typing import TYPE_CHECKING, Any, Protocol +from uuid import uuid4 import boto3 # type: ignore from aws_durable_execution_sdk_python.execution import ( @@ -26,6 +28,14 @@ from aws_durable_execution_sdk_python_testing.execution import Execution +@dataclass(frozen=True) +class InvokeResponse: + """Response from invoking a durable function.""" + + invocation_output: DurableExecutionInvocationOutput + request_id: str + + def create_test_lambda_context() -> LambdaContext: # Create client context as a dictionary, not as objects # LambdaContext.__init__ expects dictionaries and will create the objects internally @@ -65,7 +75,7 @@ def invoke( function_name: str, input: DurableExecutionInvocationInput, endpoint_url: str | None = None, - ) -> DurableExecutionInvocationOutput: ... # pragma: no cover + ) -> InvokeResponse: ... # pragma: no cover def update_endpoint( self, endpoint_url: str, region_name: str @@ -96,14 +106,17 @@ def invoke( function_name: str, # noqa: ARG002 input: DurableExecutionInvocationInput, endpoint_url: str | None = None, # noqa: ARG002 - ) -> DurableExecutionInvocationOutput: + ) -> InvokeResponse: # TODO: reasses if function_name will be used in future input_with_client = DurableExecutionInvocationInputWithClient.from_durable_execution_invocation_input( input, self.service_client ) context = create_test_lambda_context() response_dict = self.handler(input_with_client, context) - return DurableExecutionInvocationOutput.from_dict(response_dict) + output = DurableExecutionInvocationOutput.from_dict(response_dict) + return InvokeResponse( + invocation_output=output, request_id=context.aws_request_id + ) def update_endpoint(self, endpoint_url: str, region_name: str) -> None: """No-op for in-process invoker.""" @@ -192,7 +205,7 @@ def invoke( function_name: str, input: DurableExecutionInvocationInput, endpoint_url: str | None = None, - ) -> DurableExecutionInvocationOutput: + ) -> InvokeResponse: """Invoke AWS Lambda function and return durable execution result. Args: @@ -201,7 +214,7 @@ def invoke( endpoint_url: Lambda endpoint url Returns: - DurableExecutionInvocationOutput: Result of the function execution + InvokeResponse: Response containing invocation output and request ID Raises: ResourceNotFoundException: If function does not exist @@ -247,8 +260,17 @@ def invoke( response_payload = response["Payload"].read().decode("utf-8") response_dict = json.loads(response_payload) + # Extract request ID from response headers (x-amzn-RequestId or x-amzn-request-id) + headers = response.get("ResponseMetadata", {}).get("HTTPHeaders", {}) + request_id = ( + headers.get("x-amzn-RequestId") + or headers.get("x-amzn-request-id") + or f"local-{uuid4()}" + ) + # Convert to DurableExecutionInvocationOutput - return DurableExecutionInvocationOutput.from_dict(response_dict) + output = DurableExecutionInvocationOutput.from_dict(response_dict) + return InvokeResponse(invocation_output=output, request_id=request_id) except client.exceptions.ResourceNotFoundException as e: msg = f"Function not found: {function_name}" diff --git a/src/aws_durable_execution_sdk_python_testing/model.py b/src/aws_durable_execution_sdk_python_testing/model.py index 87f624c..0353870 100644 --- a/src/aws_durable_execution_sdk_python_testing/model.py +++ b/src/aws_durable_execution_sdk_python_testing/model.py @@ -68,6 +68,7 @@ class EventType(Enum): CALLBACK_SUCCEEDED = "CallbackSucceeded" CALLBACK_FAILED = "CallbackFailed" CALLBACK_TIMED_OUT = "CallbackTimedOut" + INVOCATION_COMPLETED = "InvocationCompleted" TERMINAL_STATUSES: set[OperationStatus] = { @@ -1222,6 +1223,30 @@ def to_dict(self) -> dict[str, Any]: return result +@dataclass(frozen=True) +class InvocationCompletedDetails: + """Invocation completed event details.""" + + start_timestamp: datetime.datetime + end_timestamp: datetime.datetime + request_id: str + + @classmethod + def from_dict(cls, data: dict) -> InvocationCompletedDetails: + return cls( + start_timestamp=data["StartTimestamp"], + end_timestamp=data["EndTimestamp"], + request_id=data["RequestId"], + ) + + def to_dict(self) -> dict[str, Any]: + return { + "StartTimestamp": self.start_timestamp, + "EndTimestamp": self.end_timestamp, + "RequestId": self.request_id, + } + + # endregion event_structures @@ -1329,6 +1354,7 @@ class Event: callback_succeeded_details: CallbackSucceededDetails | None = None callback_failed_details: CallbackFailedDetails | None = None callback_timed_out_details: CallbackTimedOutDetails | None = None + invocation_completed_details: InvocationCompletedDetails | None = None @classmethod def from_dict(cls, data: dict) -> Event: @@ -1447,6 +1473,12 @@ def from_dict(cls, data: dict) -> Event: if details_data := data.get("CallbackTimedOutDetails"): callback_timed_out_details = CallbackTimedOutDetails.from_dict(details_data) + invocation_completed_details = None + if details_data := data.get("InvocationCompletedDetails"): + invocation_completed_details = InvocationCompletedDetails.from_dict( + details_data + ) + return cls( event_type=data["EventType"], event_timestamp=data["EventTimestamp"], @@ -1479,6 +1511,7 @@ def from_dict(cls, data: dict) -> Event: callback_succeeded_details=callback_succeeded_details, callback_failed_details=callback_failed_details, callback_timed_out_details=callback_timed_out_details, + invocation_completed_details=invocation_completed_details, ) def to_dict(self) -> dict[str, Any]: @@ -1563,6 +1596,10 @@ def to_dict(self) -> dict[str, Any]: result["CallbackTimedOutDetails"] = ( self.callback_timed_out_details.to_dict() ) + if self.invocation_completed_details is not None: + result["InvocationCompletedDetails"] = ( + self.invocation_completed_details.to_dict() + ) return result # region execution @@ -2218,6 +2255,30 @@ def create_callback_event(cls, context: EventCreationContext) -> Event: # endregion callback + # region invocation_completed + @classmethod + def create_invocation_completed( + cls, + event_id: int, + event_timestamp: datetime.datetime, + start_timestamp: datetime.datetime, + end_timestamp: datetime.datetime, + request_id: str, + ) -> Event: + """Create invocation completed event.""" + return cls( + event_type=EventType.INVOCATION_COMPLETED.value, + event_timestamp=event_timestamp, + event_id=event_id, + invocation_completed_details=InvocationCompletedDetails( + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + request_id=request_id, + ), + ) + + # endregion invocation_completed + @classmethod def create_event_started(cls, context: EventCreationContext) -> Event: """Convert operation to started event.""" diff --git a/tests/executor_test.py b/tests/executor_test.py index dad04f0..295248f 100644 --- a/tests/executor_test.py +++ b/tests/executor_test.py @@ -34,6 +34,7 @@ Execution, ) from aws_durable_execution_sdk_python_testing.executor import Executor +from aws_durable_execution_sdk_python_testing.invoker import InvokeResponse from aws_durable_execution_sdk_python_testing.model import ( ListDurableExecutionsResponse, SendDurableExecutionCallbackFailureResponse, @@ -285,7 +286,9 @@ def test_should_complete_workflow_with_error_when_invocation_fails( failed_response = DurableExecutionInvocationOutput( status=InvocationStatus.FAILED, error=ErrorObject.from_message("Test error") ) - mock_invoker.invoke.return_value = failed_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=failed_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -329,7 +332,9 @@ def test_should_complete_workflow_with_result_when_invocation_succeeds( success_response = DurableExecutionInvocationOutput( status=InvocationStatus.SUCCEEDED, result="success result" ) - mock_invoker.invoke.return_value = success_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=success_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -372,7 +377,9 @@ def test_should_handle_pending_status_when_operations_exist( mock_invocation_input = Mock() mock_invoker.create_invocation_input.return_value = mock_invocation_input pending_response = DurableExecutionInvocationOutput(status=InvocationStatus.PENDING) - mock_invoker.invoke.return_value = pending_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=pending_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -409,8 +416,9 @@ def test_should_ignore_response_when_execution_already_complete( # Mock invoker - this shouldn't be called since execution is complete mock_invoker.create_invocation_input.return_value = Mock() - mock_invoker.invoke.return_value = DurableExecutionInvocationOutput( - status=InvocationStatus.SUCCEEDED + mock_invoker.invoke.return_value = ( + DurableExecutionInvocationOutput(status=InvocationStatus.SUCCEEDED), + "test-request-id", ) # Mock execution creation and store behavior @@ -452,7 +460,9 @@ def test_should_retry_when_response_has_no_status( mock_invocation_input = Mock() mock_invoker.create_invocation_input.return_value = mock_invocation_input no_status_response = DurableExecutionInvocationOutput(status=None) - mock_invoker.invoke.return_value = no_status_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=no_status_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -495,7 +505,9 @@ def test_should_retry_when_failed_response_has_result( invalid_response = DurableExecutionInvocationOutput( status=InvocationStatus.FAILED, result="should not have result" ) - mock_invoker.invoke.return_value = invalid_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=invalid_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -539,7 +551,9 @@ def test_should_retry_when_success_response_has_error( status=InvocationStatus.SUCCEEDED, error=ErrorObject.from_message("should not have error"), ) - mock_invoker.invoke.return_value = invalid_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=invalid_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -581,7 +595,9 @@ def test_should_retry_when_pending_response_has_no_operations( mock_invocation_input = Mock() mock_invoker.create_invocation_input.return_value = mock_invocation_input pending_response = DurableExecutionInvocationOutput(status=InvocationStatus.PENDING) - mock_invoker.invoke.return_value = pending_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=pending_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -622,7 +638,9 @@ def test_invoke_handler_success( mock_response = DurableExecutionInvocationOutput( status=InvocationStatus.SUCCEEDED, result="test" ) - mock_invoker.invoke.return_value = mock_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=mock_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -694,7 +712,9 @@ def test_invoke_handler_execution_completed_during_invocation( mock_invocation_input = Mock() mock_invoker.create_invocation_input.return_value = mock_invocation_input mock_response = Mock() - mock_invoker.invoke.return_value = mock_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=mock_response, request_id="test-request-id" + ) # Create a completed execution mock completed_execution = Mock() @@ -1037,7 +1057,14 @@ def test_should_retry_invocation_when_under_limit_through_public_api( success_response = DurableExecutionInvocationOutput( status=InvocationStatus.SUCCEEDED, result="final success" ) - mock_invoker.invoke.side_effect = [invalid_response, success_response] + mock_invoker.invoke.side_effect = [ + InvokeResponse( + invocation_output=invalid_response, request_id="test-request-id-1" + ), + InvokeResponse( + invocation_output=success_response, request_id="test-request-id-2" + ), + ] # Mock execution creation and store behavior with patch( @@ -1435,7 +1462,9 @@ def test_should_retry_when_response_has_unexpected_status( mock_invoker.create_invocation_input.return_value = mock_invocation_input unexpected_response = Mock() unexpected_response.status = "UNKNOWN_STATUS" - mock_invoker.invoke.return_value = unexpected_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=unexpected_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -1480,7 +1509,9 @@ def test_invoke_handler_execution_completed_during_invocation_async( mock_invocation_input = Mock() mock_invoker.create_invocation_input.return_value = mock_invocation_input mock_response = Mock() - mock_invoker.invoke.return_value = mock_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=mock_response, request_id="test-request-id" + ) # Mock execution creation with patch( @@ -1566,7 +1597,9 @@ def test_invoke_handler_general_exception_async( success_response = DurableExecutionInvocationOutput( status=InvocationStatus.SUCCEEDED, result="success" ) - mock_invoker.invoke.return_value = success_response + mock_invoker.invoke.return_value = InvokeResponse( + invocation_output=success_response, request_id="test-request-id" + ) # Mock execution creation and store behavior with patch( @@ -2094,6 +2127,7 @@ def test_get_execution_history(executor, mock_store): mock_execution = Mock() mock_execution.operations = [] # Empty operations list mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2123,6 +2157,7 @@ def test_get_execution_history_with_events(executor, mock_store): mock_execution = Mock() mock_execution.operations = [op1] mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2148,6 +2183,7 @@ def test_get_execution_history_reverse_order(executor, mock_store): mock_execution = Mock() mock_execution.operations = [op1] mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2178,6 +2214,7 @@ def test_get_execution_history_pagination(executor, mock_store): mock_execution = Mock() mock_execution.operations = operations mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2206,6 +2243,7 @@ def test_get_execution_history_pagination_with_marker(executor, mock_store): mock_execution = Mock() mock_execution.operations = operations mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2223,6 +2261,7 @@ def test_get_execution_history_invalid_marker(executor, mock_store): mock_execution = Mock() mock_execution.operations = [] mock_execution.updates = [] + mock_execution.invocation_completions = [] mock_execution.durable_execution_arn = "" mock_execution.start_input = Mock() mock_execution.result = Mock() @@ -2399,6 +2438,7 @@ def test_send_callback_heartbeat(executor, mock_store): mock_operation.status = OperationStatus.STARTED mock_execution.find_callback_operation.return_value = (0, mock_operation) mock_execution.updates = [] # No callback options to reset timeout + mock_execution.invocation_completions = [] mock_store.load.return_value = mock_execution result = executor.send_callback_heartbeat(callback_id) @@ -2651,6 +2691,7 @@ def test_schedule_callback_timeouts_no_callback_options(executor, mock_store): mock_execution = Mock() mock_execution.find_operation.return_value = (0, operation) mock_execution.updates = [] # No updates with callback options + mock_execution.invocation_completions = [] mock_store.load.return_value = mock_execution # Should return early without scheduling diff --git a/tests/invoker_test.py b/tests/invoker_test.py index e7fe4e1..09c62a6 100644 --- a/tests/invoker_test.py +++ b/tests/invoker_test.py @@ -87,11 +87,12 @@ def test_in_process_invoker_invoke(): initial_execution_state=InitialExecutionState(operations=[], next_marker=""), ) - result = invoker.invoke("test-function", input_data) + response = invoker.invoke("test-function", input_data) - assert isinstance(result, DurableExecutionInvocationOutput) - assert result.status == InvocationStatus.SUCCEEDED - assert result.result == "test-result" + assert isinstance(response.invocation_output, DurableExecutionInvocationOutput) + assert response.invocation_output.status == InvocationStatus.SUCCEEDED + assert response.invocation_output.result == "test-result" + assert isinstance(response.request_id, str) # Verify handler was called with correct arguments handler.assert_called_once() @@ -162,6 +163,7 @@ def test_lambda_invoker_invoke_success(): lambda_client.invoke.return_value = { "StatusCode": 200, "Payload": mock_payload, + "ResponseMetadata": {"HTTPHeaders": {"x-amzn-RequestId": "test-request-id"}}, } invoker = LambdaInvoker(lambda_client) @@ -172,11 +174,12 @@ def test_lambda_invoker_invoke_success(): initial_execution_state=InitialExecutionState(operations=[], next_marker=""), ) - result = invoker.invoke("test-function", input_data) + response = invoker.invoke("test-function", input_data) - assert isinstance(result, DurableExecutionInvocationOutput) - assert result.status == InvocationStatus.SUCCEEDED - assert result.result == "lambda-result" + assert isinstance(response.invocation_output, DurableExecutionInvocationOutput) + assert response.invocation_output.status == InvocationStatus.SUCCEEDED + assert response.invocation_output.result == "lambda-result" + assert response.request_id == "test-request-id" # Verify lambda client was called correctly lambda_client.invoke.assert_called_once_with( @@ -237,10 +240,11 @@ def test_in_process_invoker_invoke_with_execution_operations(): execution.start() # This adds operations invocation_input = invoker.create_invocation_input(execution) - result = invoker.invoke("test-function", invocation_input) + response = invoker.invoke("test-function", invocation_input) - assert isinstance(result, DurableExecutionInvocationOutput) - assert result.status == InvocationStatus.SUCCEEDED + assert isinstance(response.invocation_output, DurableExecutionInvocationOutput) + assert isinstance(response.request_id, str) + assert response.invocation_output.status == InvocationStatus.SUCCEEDED assert len(invocation_input.initial_execution_state.operations) > 0 @@ -322,6 +326,9 @@ def test_lambda_invoker_invoke_status_202(): lambda_client.invoke.return_value = { "StatusCode": 202, "Payload": mock_payload, + "ResponseMetadata": { + "HTTPHeaders": {"x-amzn-RequestId": "test-request-id-202"} + }, } invoker = LambdaInvoker(lambda_client) @@ -332,8 +339,9 @@ def test_lambda_invoker_invoke_status_202(): initial_execution_state=InitialExecutionState(operations=[], next_marker=""), ) - result = invoker.invoke("test-function", input_data) - assert isinstance(result, DurableExecutionInvocationOutput) + response = invoker.invoke("test-function", input_data) + assert isinstance(response.invocation_output, DurableExecutionInvocationOutput) + assert response.request_id == "test-request-id-202" def test_lambda_invoker_invoke_function_error():