@@ -108,8 +108,6 @@ async def test_get_results_returns_empty_result_no_crypto(self):
108108 assert executions [0 ].result == ""
109109 assert executions [0 ].result_metadata == ""
110110 mock_crypto .assert_not_called ()
111- for call in mock_instance .list_workflows_async .call_args_list :
112- assert call .kwargs .get ("load_output" ) is False
113111
114112 @pytest .mark .asyncio
115113 async def test_get_results_sets_is_encrypted_from_content_metadata (self ):
@@ -200,6 +198,133 @@ async def test_get_results_dbos_error_no_exception_fallback(self):
200198 assert len (executions ) == 1
201199 assert executions [0 ].error # fallback string must be truthy
202200
201+ @pytest .mark .asyncio
202+ async def test_get_results_success_with_output_error_marks_failed (self ):
203+ """DBOS SUCCESS workflow whose output.error is set must surface as FAILED."""
204+ task = octobot_node .models .Task (
205+ id = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" ,
206+ name = "business-failed-task" ,
207+ content = None ,
208+ type = "execute_actions" ,
209+ )
210+ output = params .AutomationWorkflowOutput (error = "Trade rejected by exchange" )
211+ inputs = params .AutomationWorkflowInputs (task = task )
212+ ws = mock .Mock (spec = dbos .WorkflowStatus )
213+ ws .workflow_id = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
214+ ws .name = "business-failed-task"
215+ ws .status = dbos .WorkflowStatusString .SUCCESS .value
216+ ws .output = json .dumps (output .to_dict ())
217+ ws .error = None
218+ ws .input = {"args" : [inputs .to_dict ()], "kwargs" : {}}
219+ ws .created_at = None
220+ ws .updated_at = None
221+
222+ sched , mock_instance = _make_scheduler_with_mock_instance ()
223+ mock_instance .list_workflows_async = mock .AsyncMock (return_value = [ws ])
224+
225+ executions = await sched .get_results ()
226+
227+ assert len (executions ) == 1
228+ assert executions [0 ].status == octobot_node .models .TaskStatus .FAILED
229+ assert executions [0 ].error == "Trade rejected by exchange"
230+
231+ @pytest .mark .asyncio
232+ async def test_get_results_success_with_malformed_output_falls_back_to_completed (self ):
233+ """SUCCESS workflow with unparseable output JSON must NOT crash and must default to COMPLETED.
234+
235+ Regression guard: a parse exception inside the SUCCESS branch must not bubble up
236+ and must not flip the task to FAILED — the workflow ran fine at DBOS level.
237+ """
238+ task = octobot_node .models .Task (
239+ id = "ffffffff-ffff-ffff-ffff-ffffffffffff" ,
240+ name = "malformed-output-task" ,
241+ content = None ,
242+ type = "execute_actions" ,
243+ )
244+ inputs = params .AutomationWorkflowInputs (task = task )
245+ ws = mock .Mock (spec = dbos .WorkflowStatus )
246+ ws .workflow_id = "ffffffff-ffff-ffff-ffff-ffffffffffff"
247+ ws .name = "malformed-output-task"
248+ ws .status = dbos .WorkflowStatusString .SUCCESS .value
249+ ws .output = "{not valid json"
250+ ws .error = None
251+ ws .input = {"args" : [inputs .to_dict ()], "kwargs" : {}}
252+ ws .created_at = None
253+ ws .updated_at = None
254+
255+ sched , mock_instance = _make_scheduler_with_mock_instance ()
256+ mock_instance .list_workflows_async = mock .AsyncMock (return_value = [ws ])
257+
258+ executions = await sched .get_results ()
259+
260+ assert len (executions ) == 1
261+ assert executions [0 ].status == octobot_node .models .TaskStatus .COMPLETED
262+ assert executions [0 ].error is None
263+
264+ @pytest .mark .asyncio
265+ async def test_get_results_wallet_filter_keeps_legacy_task_without_wallet_address (self ):
266+ """Regression: tasks created before the multi-wallet refactor have task.wallet_address=None.
267+ They must remain visible to any caller — never dropped by the wallet filter.
268+ """
269+ legacy_task = octobot_node .models .Task (
270+ id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" ,
271+ name = "legacy-task" ,
272+ content = None ,
273+ type = "execute_actions" ,
274+ wallet_address = None , # pre-multi-tenant: no wallet attached
275+ )
276+ ws = _build_mock_workflow_status (legacy_task , None , None , workflow_id = legacy_task .id )
277+
278+ sched , mock_instance = _make_scheduler_with_mock_instance ()
279+ mock_instance .list_workflows_async = mock .AsyncMock (return_value = [ws ])
280+
281+ executions = await sched .get_results (wallet_address = "0xcaller" )
282+
283+ assert len (executions ) == 1
284+ assert executions [0 ].id == legacy_task .id
285+
286+ @pytest .mark .asyncio
287+ async def test_get_results_wallet_filter_keeps_workflow_with_unparseable_input (self ):
288+ """Regression: a crashed workflow may have unparseable input → get_input_task returns None.
289+ Must remain visible — silently dropping is what hid all errored tasks before.
290+ """
291+ ws = mock .Mock (spec = dbos .WorkflowStatus )
292+ ws .workflow_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
293+ ws .name = "crashed-task"
294+ ws .status = dbos .WorkflowStatusString .ERROR .value
295+ ws .output = None
296+ ws .error = RuntimeError ("crashed before input was persisted" )
297+ ws .input = {"args" : [], "kwargs" : {}} # nothing parseable
298+ ws .created_at = None
299+ ws .updated_at = None
300+
301+ sched , mock_instance = _make_scheduler_with_mock_instance ()
302+ mock_instance .list_workflows_async = mock .AsyncMock (return_value = [ws ])
303+
304+ executions = await sched .get_results (wallet_address = "0xcaller" )
305+
306+ assert len (executions ) == 1
307+ assert executions [0 ].status == octobot_node .models .TaskStatus .FAILED
308+
309+ @pytest .mark .asyncio
310+ async def test_get_results_wallet_filter_drops_task_with_explicit_other_wallet (self ):
311+ """Sanity check that the filter still drops tasks explicitly owned by a different wallet."""
312+ other_task = octobot_node .models .Task (
313+ id = "cccccccc-cccc-cccc-cccc-cccccccccccc" ,
314+ name = "other-task" ,
315+ content = None ,
316+ type = "execute_actions" ,
317+ wallet_address = "0xother" ,
318+ )
319+ ws = _build_mock_workflow_status (other_task , None , None , workflow_id = other_task .id )
320+
321+ sched , mock_instance = _make_scheduler_with_mock_instance ()
322+ mock_instance .list_workflows_async = mock .AsyncMock (return_value = [ws ])
323+
324+ executions = await sched .get_results (wallet_address = "0xcaller" )
325+
326+ assert executions == []
327+
203328
204329class TestGetWorkflowsExportResults :
205330
0 commit comments