diff --git a/pyproject.toml b/pyproject.toml index ec055aa..9146787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "uipath-runtime" -version = "0.11.4" +version = "0.11.5" description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" dependencies = [ - "uipath-core>=0.5.22,<0.6.0", + "uipath-core>=0.5.23,<0.6.0", ] classifiers = [ "Intended Audience :: Developers", diff --git a/src/uipath/runtime/debug/runtime.py b/src/uipath/runtime/debug/runtime.py index 6bd56e2..a64606a 100644 --- a/src/uipath/runtime/debug/runtime.py +++ b/src/uipath/runtime/debug/runtime.py @@ -194,9 +194,10 @@ async def _stream_and_debug( resume_data: dict[str, Any] | None = None try: trigger_data: dict[str, Any] | None = None - if ( - final_result.trigger.trigger_type - == UiPathResumeTriggerType.API + if final_result.trigger.trigger_type in ( + UiPathResumeTriggerType.API, + UiPathResumeTriggerType.INBOX, + UiPathResumeTriggerType.TIMER, ): trigger_data = ( await self.debug_bridge.wait_for_resume() diff --git a/src/uipath/runtime/resumable/runtime.py b/src/uipath/runtime/resumable/runtime.py index 93bc2e5..8cf2ed5 100644 --- a/src/uipath/runtime/resumable/runtime.py +++ b/src/uipath/runtime/resumable/runtime.py @@ -147,8 +147,9 @@ async def stream( async def _get_fired_triggers(self) -> dict[str, Any] | None: """Check stored triggers for any that have already fired. - Skips async-external triggers (API, Inbox) whose payloads only arrive - asynchronously and cannot be polled at suspend time. + Skips external triggers (API, Inbox, Timer) whose payloads only arrive + asynchronously or through Orchestrator resume and cannot be polled at + suspend time. Returns: A resume map of {interrupt_id: resume_data} for fired triggers, or None. @@ -161,7 +162,11 @@ async def _get_fired_triggers(self) -> dict[str, Any] | None: t for t in triggers if t.trigger_type - not in (UiPathResumeTriggerType.API, UiPathResumeTriggerType.INBOX) + not in ( + UiPathResumeTriggerType.API, + UiPathResumeTriggerType.INBOX, + UiPathResumeTriggerType.TIMER, + ) ] return await self._build_resume_map(pollable_triggers) diff --git a/tests/test_resumable.py b/tests/test_resumable.py index 59655f3..16d0c41 100644 --- a/tests/test_resumable.py +++ b/tests/test_resumable.py @@ -548,6 +548,47 @@ def create_inbox_trigger(data: dict[str, Any]) -> UiPathResumeTrigger: trigger_manager.read_trigger.assert_not_called() assert runtime_impl.execution_count == 1 + @pytest.mark.asyncio + async def test_resumable_skips_timer_triggers_on_auto_resume_check(self) -> None: + """Timer triggers should be skipped when checking for auto-resume.""" + + runtime_impl = MultiTriggerMockRuntime() + storage = StatefulStorageMock() + trigger_manager = make_trigger_manager_mock() + + def create_timer_trigger(data: dict[str, Any]) -> UiPathResumeTrigger: + return UiPathResumeTrigger( + interrupt_id="", # Will be set by resumable runtime + trigger_type=UiPathResumeTriggerType.TIMER, + payload=data, + ) + + trigger_manager.create_trigger = AsyncMock(side_effect=create_timer_trigger) # type: ignore[method-assign] + read_trigger_guard = AsyncMock( + side_effect=AssertionError( + "read_trigger must not be called for Timer triggers pre-resume" + ) + ) + trigger_manager.read_trigger = read_trigger_guard # type: ignore[method-assign] + + resumable = UiPathResumableRuntime( + delegate=runtime_impl, + storage=storage, + trigger_manager=trigger_manager, + runtime_id="runtime-1", + ) + + result = await resumable.execute({}) + + assert result.status == UiPathRuntimeStatus.SUSPENDED + assert result.triggers is not None + assert len(result.triggers) == 2 + assert all( + t.trigger_type == UiPathResumeTriggerType.TIMER for t in result.triggers + ) + trigger_manager.read_trigger.assert_not_called() + assert runtime_impl.execution_count == 1 + @pytest.mark.asyncio async def test_resumable_auto_resumes_task_triggers_but_not_api_triggers( self, diff --git a/uv.lock b/uv.lock index 0a07a22..e533134 100644 --- a/uv.lock +++ b/uv.lock @@ -998,21 +998,21 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.22" +version = "0.5.23" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/e0/1cdf0537ae1db831b066604e0e83132a2dd559371ac6e5d56e96b9039163/uipath_core-0.5.22.tar.gz", hash = "sha256:01ae7c3770369469acf5cef31908e8b878a5b1123f2d930f8537ea2d97d7d621", size = 136212, upload-time = "2026-06-23T16:18:43.081Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e7/cfa1aa2526c2d4d35ce745d2d845019aba9912fe90619560f4aa65f98b48/uipath_core-0.5.23.tar.gz", hash = "sha256:550328cdf3fa7c11f92d1d724eb6ecba7359e57b8fdea6e8fc8dea80b43bb249", size = 136320, upload-time = "2026-06-26T14:25:06.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/97/2258d51969ec71b1056d67f39d612eac2d7c6e9458d3b3c9a0b10f42e730/uipath_core-0.5.22-py3-none-any.whl", hash = "sha256:60df655b207e02a6d3bfae8c61e1fc9bc0bf11576f7ead07b8b38f23d13fc4d6", size = 58222, upload-time = "2026-06-23T16:18:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/1f11b5b762591e5bf25072b5b19834edef379c0b6daf377285e280016785/uipath_core-0.5.23-py3-none-any.whl", hash = "sha256:989392329c53562e7987e43fec8726c91a58a869d92f2c47bbae5145b11780ac", size = 58325, upload-time = "2026-06-26T14:25:05.608Z" }, ] [[package]] name = "uipath-runtime" -version = "0.11.4" +version = "0.11.5" source = { editable = "." } dependencies = [ { name = "uipath-core" }, @@ -1034,7 +1034,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "uipath-core", specifier = ">=0.5.22,<0.6.0" }] +requires-dist = [{ name = "uipath-core", specifier = ">=0.5.23,<0.6.0" }] [package.metadata.requires-dev] dev = [