Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[project]
name = "uipath"
version = "2.10.53"
version = "2.10.54"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.5.8, <0.6.0",
"uipath-runtime>=0.10.0, <0.11.0",
"uipath-runtime>=0.10.1, <0.11.0",
"uipath-platform>=0.1.13, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
Expand Down
23 changes: 19 additions & 4 deletions packages/uipath/src/uipath/_cli/_debug/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@
UiPathRuntimeResult,
UiPathRuntimeStatus,
)
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugQuitError
from uipath.runtime.debug import (
DetachedDebugBridge,
UiPathDebugProtocol,
UiPathDebugQuitError,
)
from uipath.runtime.events import UiPathRuntimeStateEvent, UiPathRuntimeStatePhase

DebugAttachMode = Literal["signalr", "console", "none"]

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -871,18 +877,27 @@ def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugProtoco


def get_debug_bridge(
context: UiPathRuntimeContext, verbose: bool = True
context: UiPathRuntimeContext,
verbose: bool = True,
attach: DebugAttachMode | None = None,
) -> UiPathDebugProtocol:
"""Factory to get appropriate debug bridge based on context.

Args:
context: The runtime context containing debug configuration.
verbose: If True, console bridge shows all state updates. If False, only breakpoints.
attach: Explicit attach mode. When None, falls back to
``context.job_id``-based selection.

Returns:
An instance of UiPathDebugBridge suitable for the context.
"""
if context.job_id:
if attach == "none":
return DetachedDebugBridge()
if attach == "signalr":
return get_remote_debug_bridge(context)
else:
if attach == "console":
return ConsoleDebugBridge(verbose=verbose)
if context.job_id:
return get_remote_debug_bridge(context)
return ConsoleDebugBridge(verbose=verbose)
22 changes: 20 additions & 2 deletions packages/uipath/src/uipath/_cli/cli_debug.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import asyncio
import logging
from typing import cast, get_args

import click

from uipath._cli._chat._bridge import get_chat_bridge
from uipath._cli._debug._bridge import get_debug_bridge
from uipath._cli._debug._bridge import DebugAttachMode, get_debug_bridge
from uipath._cli._utils._debug import setup_debugging
from uipath._cli._utils._studio_project import StudioClient
from uipath.core.tracing import UiPathTraceManager
Expand Down Expand Up @@ -64,6 +65,15 @@
default=5678,
help="Port for the debug server (default: 5678)",
)
@click.option(
"--attach",
type=click.Choice(list(get_args(DebugAttachMode)), case_sensitive=False),
default=None,
help=(
"Debugger attach mode. Defaults to 'signalr' for cloud runs, "
"'console' for local runs."
),
)
@track_command("debug")
def debug(
entrypoint: str | None,
Expand All @@ -74,13 +84,18 @@ def debug(
output_file: str | None,
debug: bool,
debug_port: int,
attach: str | None,
) -> None:
"""Debug the project."""
input_file = file or input_file
# Setup debugging if requested
if not setup_debugging(debug, debug_port):
console.error(f"Failed to start debug server on port {debug_port}")

attach_mode: DebugAttachMode | None = (
cast(DebugAttachMode, attach.lower()) if attach else None
)

result = Middlewares.next(
"debug",
entrypoint,
Expand All @@ -90,6 +105,7 @@ def debug(
output_file=output_file,
debug=debug,
debug_port=debug_port,
attach=attach_mode,
)

if result.error_message:
Expand Down Expand Up @@ -141,7 +157,9 @@ async def execute_debug_runtime():

async def execute_debug_runtime():
chat_runtime: UiPathRuntimeProtocol | None = None
debug_bridge: UiPathDebugProtocol = get_debug_bridge(ctx)
debug_bridge: UiPathDebugProtocol = get_debug_bridge(
ctx, attach=attach_mode
)

runtime = await factory.new_runtime(
entrypoint,
Expand Down
64 changes: 64 additions & 0 deletions packages/uipath/tests/cli/test_debug_bridge_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Tests for `get_debug_bridge()` selection matrix.

Locks in the non-breaking-change contract: absence of `attach` preserves the
legacy `job_id`-based selection. Explicit `attach` overrides that selection.
"""

from __future__ import annotations

import pytest

from uipath._cli._debug._bridge import (
ConsoleDebugBridge,
SignalRDebugBridge,
get_debug_bridge,
)
from uipath.runtime import UiPathRuntimeContext
from uipath.runtime.debug import DetachedDebugBridge


def _ctx(**overrides) -> UiPathRuntimeContext:
return UiPathRuntimeContext(**overrides)


def test_attach_none_returns_detached_bridge_without_job_id():
bridge = get_debug_bridge(_ctx(), attach="none")
assert isinstance(bridge, DetachedDebugBridge)


def test_attach_none_returns_detached_bridge_even_when_job_id_set(monkeypatch):
"""'none' wins over job_id — this is the whole point of the flag."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="none")
assert isinstance(bridge, DetachedDebugBridge)


def test_attach_signalr_forces_signalr_bridge(monkeypatch):
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="signalr")
assert isinstance(bridge, SignalRDebugBridge)


def test_attach_console_forces_console_bridge_even_when_job_id_set(monkeypatch):
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
bridge = get_debug_bridge(_ctx(job_id="job-123"), attach="console")
assert isinstance(bridge, ConsoleDebugBridge)


def test_legacy_selection_signalr_when_job_id_set_and_no_attach(monkeypatch):
"""Non-breaking change assertion: absence of `attach` preserves today's behavior."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
bridge = get_debug_bridge(_ctx(job_id="job-123"))
assert isinstance(bridge, SignalRDebugBridge)


def test_legacy_selection_console_when_no_job_id_and_no_attach():
"""Non-breaking change assertion: absence of `attach` preserves today's behavior."""
bridge = get_debug_bridge(_ctx())
assert isinstance(bridge, ConsoleDebugBridge)


def test_attach_signalr_without_job_id_raises():
"""Explicit signalr without job_id is a user error — surface it loudly."""
with pytest.raises(ValueError, match="UIPATH_URL and UIPATH_JOB_KEY"):
get_debug_bridge(_ctx(), attach="signalr")
10 changes: 5 additions & 5 deletions packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading