@@ -2139,6 +2139,176 @@ def orchestrator(ctx: task.OrchestrationContext, _):
21392139 assert actions [0 ].sendEntityMessage .entityOperationCalled .targetInstanceId .value == str (test_entity_id )
21402140
21412141
2142+ def test_entity_call_failure_propagated_over_old_protocol ():
2143+ """A failed entity response carrying a structured ``failureDetails`` object
2144+ (defensively supported, current-protocol shape) must surface as a
2145+ TaskFailedError with the structured message."""
2146+ test_entity_id = entities .EntityInstanceId ("Counter" , "myCounter" )
2147+
2148+ def orchestrator (ctx : task .OrchestrationContext , _ ):
2149+ try :
2150+ yield ctx .call_entity (test_entity_id , "set" , 1 )
2151+ except task .TaskFailedError as e :
2152+ return e .details .message
2153+ return "no error"
2154+
2155+ registry = worker ._Registry ()
2156+ name = registry .add_orchestrator (orchestrator )
2157+
2158+ started_events = [
2159+ helpers .new_orchestrator_started_event (),
2160+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID , None ),
2161+ ]
2162+
2163+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
2164+ result1 = executor .execute (TEST_INSTANCE_ID , [], started_events )
2165+ actions = result1 .actions
2166+ assert len (actions ) == 1
2167+ assert actions [0 ].sendEntityMessage .HasField ("entityOperationCalled" )
2168+ request_id = actions [0 ].sendEntityMessage .entityOperationCalled .requestId
2169+
2170+ # A structured FailureDetails object, when present, takes precedence over the
2171+ # WebJobs result/exceptionType fields.
2172+ response_message = {
2173+ "result" : None ,
2174+ "failureDetails" : {
2175+ "ErrorType" : "ValueError" ,
2176+ "ErrorMessage" : "Something went wrong!" ,
2177+ "StackTrace" : None ,
2178+ "InnerFailure" : None ,
2179+ "IsNonRetriable" : False ,
2180+ },
2181+ }
2182+ new_events = [
2183+ helpers .new_event_sent_event (1 , str (test_entity_id ), json .dumps ({"id" : request_id })),
2184+ helpers .new_event_raised_event (request_id , json .dumps (response_message )),
2185+ ]
2186+ result2 = executor .execute (TEST_INSTANCE_ID , started_events , new_events )
2187+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result2 .actions )
2188+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
2189+ output = json .loads (complete_action .result .value )
2190+ assert output == (
2191+ "Operation 'set' on entity '@counter@myCounter' failed with error: Something went wrong!"
2192+ )
2193+
2194+
2195+ def test_entity_call_failure_propagated_over_old_protocol_exception_type ():
2196+ """A WebJobs "old protocol" failure carries the message in the serialized
2197+ ``result`` field, while ``exceptionType`` is only the exception type name.
2198+ The surfaced message must come from ``result``, not ``exceptionType``."""
2199+ test_entity_id = entities .EntityInstanceId ("Counter" , "myCounter" )
2200+
2201+ def orchestrator (ctx : task .OrchestrationContext , _ ):
2202+ try :
2203+ yield ctx .call_entity (test_entity_id , "set" , 1 )
2204+ except task .TaskFailedError as e :
2205+ return e .details .message
2206+ return "no error"
2207+
2208+ registry = worker ._Registry ()
2209+ name = registry .add_orchestrator (orchestrator )
2210+
2211+ started_events = [
2212+ helpers .new_orchestrator_started_event (),
2213+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID , None ),
2214+ ]
2215+
2216+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
2217+ result1 = executor .execute (TEST_INSTANCE_ID , [], started_events )
2218+ actions = result1 .actions
2219+ assert len (actions ) == 1
2220+ request_id = actions [0 ].sendEntityMessage .entityOperationCalled .requestId
2221+
2222+ # Real WebJobs shape: message serialized into "result"; "exceptionType" is the
2223+ # (assembly-qualified) exception type name and must NOT be used as the message.
2224+ response_message = {
2225+ "result" : json .dumps ("Something went wrong!" ),
2226+ "exceptionType" : "System.InvalidOperationException, mscorlib" ,
2227+ }
2228+ new_events = [
2229+ helpers .new_event_sent_event (1 , str (test_entity_id ), json .dumps ({"id" : request_id })),
2230+ helpers .new_event_raised_event (request_id , json .dumps (response_message )),
2231+ ]
2232+ result2 = executor .execute (TEST_INSTANCE_ID , started_events , new_events )
2233+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result2 .actions )
2234+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
2235+ output = json .loads (complete_action .result .value )
2236+ assert output == (
2237+ "Operation 'set' on entity '@counter@myCounter' failed with error: Something went wrong!"
2238+ )
2239+ # Guard against the regression of surfacing the type name instead of the message.
2240+ assert "InvalidOperationException" not in output
2241+
2242+
2243+ def test_entity_call_success_over_old_protocol ():
2244+ """A successful entity operation delivered via the legacy protocol must
2245+ complete the call_entity task with the unwrapped result."""
2246+ test_entity_id = entities .EntityInstanceId ("Counter" , "myCounter" )
2247+
2248+ def orchestrator (ctx : task .OrchestrationContext , _ ):
2249+ return (yield ctx .call_entity (test_entity_id , "get" , return_type = int ))
2250+
2251+ registry = worker ._Registry ()
2252+ name = registry .add_orchestrator (orchestrator )
2253+
2254+ started_events = [
2255+ helpers .new_orchestrator_started_event (),
2256+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID , None ),
2257+ ]
2258+
2259+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
2260+ result1 = executor .execute (TEST_INSTANCE_ID , [], started_events )
2261+ actions = result1 .actions
2262+ assert len (actions ) == 1
2263+ request_id = actions [0 ].sendEntityMessage .entityOperationCalled .requestId
2264+
2265+ response_message = {"result" : json .dumps (42 )}
2266+ new_events = [
2267+ helpers .new_event_sent_event (1 , str (test_entity_id ), json .dumps ({"id" : request_id })),
2268+ helpers .new_event_raised_event (request_id , json .dumps (response_message )),
2269+ ]
2270+ result2 = executor .execute (TEST_INSTANCE_ID , started_events , new_events )
2271+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result2 .actions )
2272+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
2273+ assert json .loads (complete_action .result .value ) == 42
2274+
2275+
2276+ @pytest .mark .parametrize ("entity_result" , ["done" , 42 , 3.5 , True , {"a" : 1 }, [1 , 2 , 3 ]])
2277+ def test_entity_call_success_over_old_protocol_round_trips_result (entity_result ):
2278+ """The legacy-protocol ``result`` field holds a *serialized* JSON string, so
2279+ the caller must receive the fully deserialized value regardless of type and
2280+ without needing an explicit ``return_type`` (i.e. no double-encoding)."""
2281+ test_entity_id = entities .EntityInstanceId ("Counter" , "myCounter" )
2282+
2283+ def orchestrator (ctx : task .OrchestrationContext , _ ):
2284+ return (yield ctx .call_entity (test_entity_id , "get" ))
2285+
2286+ registry = worker ._Registry ()
2287+ name = registry .add_orchestrator (orchestrator )
2288+
2289+ started_events = [
2290+ helpers .new_orchestrator_started_event (),
2291+ helpers .new_execution_started_event (name , TEST_INSTANCE_ID , None ),
2292+ ]
2293+
2294+ executor = worker ._OrchestrationExecutor (registry , TEST_LOGGER , JsonDataConverter ())
2295+ result1 = executor .execute (TEST_INSTANCE_ID , [], started_events )
2296+ actions = result1 .actions
2297+ assert len (actions ) == 1
2298+ request_id = actions [0 ].sendEntityMessage .entityOperationCalled .requestId
2299+
2300+ # The entity worker places the *serialized* return value into the "result" field.
2301+ response_message = {"result" : json .dumps (entity_result )}
2302+ new_events = [
2303+ helpers .new_event_sent_event (1 , str (test_entity_id ), json .dumps ({"id" : request_id })),
2304+ helpers .new_event_raised_event (request_id , json .dumps (response_message )),
2305+ ]
2306+ result2 = executor .execute (TEST_INSTANCE_ID , started_events , new_events )
2307+ complete_action = get_and_validate_complete_orchestration_action_list (1 , result2 .actions )
2308+ assert complete_action .orchestrationStatus == pb .ORCHESTRATION_STATUS_COMPLETED
2309+ assert json .loads (complete_action .result .value ) == entity_result
2310+
2311+
21422312def get_and_validate_complete_orchestration_action_list (expected_action_count : int , actions : list [pb .OrchestratorAction ]) -> pb .CompleteOrchestrationAction :
21432313 assert len (actions ) == expected_action_count
21442314 assert type (actions [- 1 ]) is pb .OrchestratorAction
0 commit comments