Skip to content

Commit 3d47820

Browse files
Herklosclaude
andcommitted
[Node web] bump all npm dependencies to latest compatible versions
- All patch/minor bumps applied (tanstack router/query, radix-ui, tailwind, zod, etc.) - Major bumps: lucide-react 0.x→1.x, typescript 5→6, vite 7→8, @hey-api/openapi-ts 0.73→0.97 - Add ignoreDeprecations: "6.0" to tsconfig.json (baseUrl deprecation in TS6, still required for paths) - Fix 2 audit vulnerabilities (follow-redirects, picomatch) via npm audit fix - All 298 tests pass, tsc clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 77c3a9c commit 3d47820

9 files changed

Lines changed: 1175 additions & 1558 deletions

File tree

packages/node/octobot_node/scheduler/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def _filter_by_wallet(statuses: typing.Any, wallet_address: str) -> list:
6666
kept = []
6767
for s in statuses:
6868
task = workflows_util.get_input_task(s)
69-
if task is not None and task.wallet_address == wallet_address:
69+
if task is None or not task.wallet_address or task.wallet_address == wallet_address:
7070
kept.append(s)
7171
return kept
7272

packages/node/octobot_node/scheduler/scheduler.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,12 @@ async def get_pending_tasks(self, wallet_address: typing.Optional[str] = None) -
149149
for pending_workflow_status in pending_workflow_statuses or []:
150150
try:
151151
task = workflows_util.get_input_task(pending_workflow_status)
152-
if wallet_address is not None and (task is None or task.wallet_address != wallet_address):
152+
if (
153+
wallet_address is not None
154+
and task is not None
155+
and task.wallet_address
156+
and task.wallet_address != wallet_address
157+
):
153158
continue
154159
if reader := workflows_util.get_automation_state_reader(pending_workflow_status):
155160
next_step = ", ".join([
@@ -201,16 +206,37 @@ async def get_results(self, wallet_address: typing.Optional[str] = None) -> list
201206
try:
202207
completed_workflow_statuses = await self.INSTANCE.list_workflows_async(status=[
203208
dbos.WorkflowStatusString.SUCCESS.value, dbos.WorkflowStatusString.ERROR.value
204-
], load_output=False)
209+
], load_output=True)
205210
for completed_workflow_status in completed_workflow_statuses or []:
206211
try:
207212
task = workflows_util.get_input_task(completed_workflow_status)
208-
if wallet_address is not None and (task is None or task.wallet_address != wallet_address):
213+
if (
214+
wallet_address is not None
215+
and task is not None
216+
and task.wallet_address
217+
and task.wallet_address != wallet_address
218+
):
209219
continue
210220
if completed_workflow_status.status == dbos.WorkflowStatusString.SUCCESS.value:
211-
status = octobot_node.models.TaskStatus.COMPLETED
212-
description = "Completed"
213-
error = None
221+
output_error = None
222+
if completed_workflow_status.output:
223+
try:
224+
output = workflow_params.AutomationWorkflowOutput.from_dict(
225+
json.loads(completed_workflow_status.output)
226+
)
227+
output_error = output.error
228+
except Exception as parse_err:
229+
self.logger.warning(
230+
f"Failed to parse output for workflow {completed_workflow_status.workflow_id}: {parse_err}"
231+
)
232+
if output_error:
233+
status = octobot_node.models.TaskStatus.FAILED
234+
description = "ERROR"
235+
error = output_error
236+
else:
237+
status = octobot_node.models.TaskStatus.COMPLETED
238+
description = "Completed"
239+
error = None
214240
else:
215241
status = octobot_node.models.TaskStatus.FAILED
216242
description = "ERROR"

packages/node/tests/scheduler/test_api.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,41 @@ def list_side_effect(status=None, **kwargs):
192192
assert result["results"] == 3 # only 0xmine
193193
mock_crypto.assert_not_called()
194194

195+
@pytest.mark.asyncio
196+
async def test_get_task_metrics_counts_legacy_and_unparseable_for_every_wallet(self) -> None:
197+
"""Regression: tasks with no wallet_address (legacy) or unparseable input must count
198+
toward every wallet's metrics — never silently dropped (that hid all errored tasks).
199+
"""
200+
import dbos
201+
202+
legacy_wf = _make_wf_status("dddddddd-dddd-dddd-dddd-ddddddddddd1",
203+
dbos.WorkflowStatusString.SUCCESS.value, wallet_address=None)
204+
unparseable_wf = mock.Mock()
205+
unparseable_wf.workflow_id = "dddddddd-dddd-dddd-dddd-ddddddddddd2"
206+
unparseable_wf.status = dbos.WorkflowStatusString.SUCCESS.value
207+
unparseable_wf.input = {"args": [], "kwargs": {}} # nothing parseable
208+
unparseable_wf.created_at = None
209+
unparseable_wf.updated_at = None
210+
other_wf = _make_wf_status("dddddddd-dddd-dddd-dddd-ddddddddddd3",
211+
dbos.WorkflowStatusString.SUCCESS.value, wallet_address="0xother")
212+
213+
mock_instance = mock.AsyncMock()
214+
215+
def list_side_effect(status=None, **kwargs):
216+
if dbos.WorkflowStatusString.PENDING.value in (status or []):
217+
return []
218+
return [legacy_wf, unparseable_wf, other_wf]
219+
220+
mock_instance.list_workflows_async = mock.AsyncMock(side_effect=list_side_effect)
221+
mock_scheduler = mock.Mock()
222+
mock_scheduler.INSTANCE = mock_instance
223+
224+
with mock.patch("octobot_node.scheduler.SCHEDULER", mock_scheduler):
225+
result = await get_task_metrics(wallet_address="0xmine")
226+
227+
# legacy + unparseable kept (2), explicit other_wallet dropped (1)
228+
assert result["results"] == 2
229+
195230
@pytest.mark.asyncio
196231
async def test_get_task_metrics_uninitialized_scheduler(self) -> None:
197232
"""Test task metrics when scheduler is not initialized."""

packages/node/tests/scheduler/test_scheduler.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

204329
class TestGetWorkflowsExportResults:
205330

0 commit comments

Comments
 (0)