diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 2d50973d8..36550f54d 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.67" +version = "2.10.68" 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" diff --git a/packages/uipath/src/uipath/_cli/cli_debug.py b/packages/uipath/src/uipath/_cli/cli_debug.py index cd9042b78..15b5ea6e5 100644 --- a/packages/uipath/src/uipath/_cli/cli_debug.py +++ b/packages/uipath/src/uipath/_cli/cli_debug.py @@ -3,13 +3,18 @@ from typing import cast, get_args import click +from pydantic import ValidationError from uipath._cli._chat._bridge import get_chat_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 -from uipath.eval.mocks import UiPathMockRuntime +from uipath.eval.mocks import ( + SimulationConfig, + UiPathMockRuntime, + build_mocking_context, +) from uipath.eval.mocks._mock_runtime import load_simulation_config from uipath.platform.common import ResourceOverwritesContext, UiPathConfig from uipath.runtime import ( @@ -74,6 +79,12 @@ "'console' for local runs." ), ) +@click.option( + "--simulation", + required=False, + default=None, + help="Simulation config as a JSON object (same schema as simulation.json)", +) @track_command("debug") def debug( entrypoint: str | None, @@ -85,6 +96,7 @@ def debug( debug: bool, debug_port: int, attach: str | None, + simulation: str | None, ) -> None: """Debug the project.""" input_file = file or input_file @@ -96,6 +108,14 @@ def debug( cast(DebugAttachMode, attach.lower()) if attach else None ) + simulation_config: SimulationConfig | None = None + if simulation: + try: + simulation_config = SimulationConfig.model_validate_json(simulation) + except (ValidationError, ValueError) as e: + console.error(f"Invalid --simulation config: {e}") + return + result = Middlewares.next( "debug", entrypoint, @@ -188,9 +208,14 @@ async def execute_debug_runtime(): if schema.metadata and "settings" in schema.metadata: agent_model = schema.metadata["settings"].get("model") - mocking_context = load_simulation_config( - agent_model=agent_model - ) + if simulation_config: + mocking_context = build_mocking_context( + simulation_config, agent_model + ) + else: + mocking_context = load_simulation_config( + agent_model=agent_model + ) mock_runtime = UiPathMockRuntime( delegate=debug_runtime, diff --git a/packages/uipath/tests/cli/test_debug_simulation.py b/packages/uipath/tests/cli/test_debug_simulation.py index d9266327b..3d9bf2516 100644 --- a/packages/uipath/tests/cli/test_debug_simulation.py +++ b/packages/uipath/tests/cli/test_debug_simulation.py @@ -453,3 +453,169 @@ def test_handles_tool_name_normalization(self, temp_dir: str): assert is_tool_simulated("Web Search") is True clear_execution_context() + + +class TestDebugSimulationFlag: + """Tests for the --simulation flag on the debug command. + + Mirrors TestRunSimulation from test_run.py for the parallel `uipath run` + flag added in PR #1624. + """ + + _SIMULATION_JSON = { + "enabled": True, + "toolsToSimulate": [{"name": "check_syntax"}, {"name": "check_style"}], + "instructions": "Simulate.", + } + + def _patch_runtime_stack(self): + """Build the patch context shared across debug-flag tests. + + Returns a tuple of (patches, mock_classes) where patches is a list of + contextmanagers to enter and mock_classes is a dict exposing the + instantiation mocks the test wants to assert against. + """ + factory = Mock() + inner_runtime = Mock() + inner_runtime.dispose = AsyncMock() + inner_runtime.get_schema = AsyncMock(return_value=Mock(metadata=None)) + factory.new_runtime = AsyncMock(return_value=inner_runtime) + factory.get_settings = AsyncMock(return_value=Mock(trace_settings=None)) + factory.dispose = AsyncMock() + + return factory, inner_runtime + + def test_invalid_simulation_json_surfaces_error( + self, runner: CliRunner, temp_dir: str + ): + """Malformed --simulation JSON is rejected before any runtime starts.""" + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + with patch( + "uipath._cli.cli_debug.UiPathRuntimeFactoryRegistry.get" + ) as factory_get: + result = runner.invoke( + cli, + ["debug", "main", "--simulation", "{ not valid json }"], + ) + + assert "Invalid --simulation config" in result.output + # We bailed before constructing any runtime. + assert not factory_get.called + + def test_simulation_flag_builds_mocking_context_from_config( + self, runner: CliRunner, temp_dir: str + ): + """--simulation triggers build_mocking_context; the file loader is bypassed.""" + factory, _ = self._patch_runtime_stack() + sentinel_context = Mock(spec=MockingContext) + + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + with ( + patch( + "uipath._cli.cli_debug.Middlewares.next", + return_value=MiddlewareResult( + should_continue=True, + error_message=None, + should_include_stacktrace=False, + ), + ), + patch( + "uipath._cli.cli_debug.UiPathRuntimeFactoryRegistry.get", + return_value=factory, + ), + patch("uipath._cli.cli_debug.get_debug_bridge"), + patch("uipath._cli.cli_debug.UiPathDebugRuntime") as debug_cls, + patch( + "uipath._cli.cli_debug.build_mocking_context", + return_value=sentinel_context, + ) as build_ctx, + patch("uipath._cli.cli_debug.load_simulation_config") as load_cfg, + patch("uipath._cli.cli_debug.UiPathMockRuntime") as mock_cls, + ): + debug_cls.return_value = Mock( + execute=AsyncMock(return_value=Mock()), + dispose=AsyncMock(), + ) + mock_cls.return_value = Mock( + execute=AsyncMock(return_value=Mock()), + dispose=AsyncMock(), + ) + + runner.invoke( + cli, + [ + "debug", + "main", + "--simulation", + json.dumps(self._SIMULATION_JSON), + ], + ) + + assert build_ctx.called + # SimulationConfig should be the first positional arg. + passed_config = build_ctx.call_args.args[0] + assert passed_config.enabled is True + assert [t.name for t in passed_config.tools_to_simulate] == [ + "check_syntax", + "check_style", + ] + assert not load_cfg.called + assert mock_cls.call_args.kwargs["mocking_context"] is sentinel_context + + def test_no_simulation_flag_falls_back_to_disk_loader( + self, runner: CliRunner, temp_dir: str + ): + """Without --simulation, cli_debug delegates to load_simulation_config.""" + factory, _ = self._patch_runtime_stack() + + with runner.isolated_filesystem(temp_dir=temp_dir): + with open("uipath.json", "w") as f: + json.dump({"functions": {"main": "main.py:main"}}, f) + with open("main.py", "w") as f: + f.write("async def main(input): return {}") + + with ( + patch( + "uipath._cli.cli_debug.Middlewares.next", + return_value=MiddlewareResult( + should_continue=True, + error_message=None, + should_include_stacktrace=False, + ), + ), + patch( + "uipath._cli.cli_debug.UiPathRuntimeFactoryRegistry.get", + return_value=factory, + ), + patch("uipath._cli.cli_debug.get_debug_bridge"), + patch("uipath._cli.cli_debug.UiPathDebugRuntime") as debug_cls, + patch("uipath._cli.cli_debug.build_mocking_context") as build_ctx, + patch( + "uipath._cli.cli_debug.load_simulation_config", + return_value=None, + ) as load_cfg, + patch("uipath._cli.cli_debug.UiPathMockRuntime") as mock_cls, + ): + debug_cls.return_value = Mock( + execute=AsyncMock(return_value=Mock()), + dispose=AsyncMock(), + ) + mock_cls.return_value = Mock( + execute=AsyncMock(return_value=Mock()), + dispose=AsyncMock(), + ) + + runner.invoke(cli, ["debug", "main"]) + + assert load_cfg.called + assert not build_ctx.called diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d7e9a9ee9..e101dff86 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-05-17T16:21:47.0725551Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2552,7 +2552,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.67" +version = "2.10.68" source = { editable = "." } dependencies = [ { name = "applicationinsights" },