diff --git a/packages/uipath-platform/CLAUDE.md b/packages/uipath-platform/CLAUDE.md index 368b9fa1e..0a81b30d9 100644 --- a/packages/uipath-platform/CLAUDE.md +++ b/packages/uipath-platform/CLAUDE.md @@ -96,7 +96,7 @@ Services provide both sync and async variants (e.g., `.invoke()` and `.invoke_as | `action_center/` | Task management for human-in-the-loop workflows | | `agenthub/` | System agents and LLM model discovery | | `automation_tracker/` | Business Transaction Service (BTS) for Process Mining | -| `business_rules/` | Business rule runs for rules deployed to Orchestrator, behind one `run()` | +| `business_rules/` | Business rule runs: rules deployed to Orchestrator, or undeployed rules in a Studio project, behind one `run()` | | `chat/` | LLM gateway, conversations, throttling | | `connections/` | External connection management | | `context_grounding/` | RAG services (DeepRAG, batch RAG, ephemeral indexes) | diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 3df1c4def..1ee12a6c8 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/business_rules/__init__.py b/packages/uipath-platform/src/uipath/platform/business_rules/__init__.py index 889f86cb3..cb8254335 100644 --- a/packages/uipath-platform/src/uipath/platform/business_rules/__init__.py +++ b/packages/uipath-platform/src/uipath/platform/business_rules/__init__.py @@ -1,8 +1,8 @@ """Business Rules service package. -Provides the ``BusinessRulesService`` client for running UiPath Business Rules -deployed to Orchestrator, and the Pydantic models for its results and trace -context. +Provides the ``BusinessRulesService`` client for running UiPath Business Rules, +either deployed to Orchestrator or read undeployed from a Studio project, and +the Pydantic models for its debug context, results and trace context. """ from ._business_rules_service import BusinessRulesService @@ -11,6 +11,7 @@ BusinessRuleError, BusinessRuleRunResult, BusinessRuleStatus, + DebugRunContext, RunMode, TraceContext, ) @@ -21,6 +22,7 @@ "BusinessRuleRunResult", "BusinessRuleStatus", "BusinessRulesService", + "DebugRunContext", "RunMode", "TraceContext", ] diff --git a/packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py b/packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py index 808dd257d..f03035377 100644 --- a/packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py +++ b/packages/uipath-platform/src/uipath/platform/business_rules/_business_rules_service.py @@ -1,6 +1,7 @@ """Business Rules service for UiPath Platform. -Runs business rules deployed to Orchestrator. +Runs business rules: a rule deployed to Orchestrator, or an undeployed rule +read straight from a Studio project. """ from collections.abc import Mapping @@ -10,7 +11,7 @@ from ..common._base_service import _TRACE_PARENT_HEADER, BaseService from ..common._bindings import resource_override -from ..common._config import UiPathApiConfig +from ..common._config import UiPathApiConfig, UiPathConfig from ..common._execution_context import UiPathExecutionContext from ..common._folder_context import FolderContext from ..common._models import Endpoint, RequestSpec @@ -21,6 +22,7 @@ BusinessRuleError, BusinessRuleRunResult, BusinessRuleStatus, + DebugRunContext, RunMode, TraceContext, _WireResponse, @@ -28,6 +30,10 @@ ) _EVALUATE_ENDPOINT = Endpoint("businessrules_/v1/business-rules/evaluate") +_DEBUG_EVALUATE_ENDPOINT = Endpoint("businessrules_/v1/business-rules/debug/evaluate") + +_HEADER_ORGANIZATION_UNIT_ID = "x-uipath-organizationunitid" +_HEADER_JOB_KEY = "x-uipath-jobkey" _HEADER_ORGANIZATION_UNIT_ID = "x-uipath-organizationunitid" @@ -40,9 +46,10 @@ class BusinessRulesService(FolderContext, BaseService): """Service for running UiPath Business Rules. - Each call runs one input against a business rule deployed to Orchestrator, - named like any other resource, and returns the decisions it produced. The - caller never picks a service endpoint. + Each call runs one input against a business rule, named like any other + resource, and returns the decisions it produced. By default the rule + deployed to Orchestrator runs; pass ``debug`` to run an undeployed rule from + a Studio project instead. The caller never picks a service endpoint. """ def __init__( @@ -61,6 +68,7 @@ def run( input: Dict[str, Any], *, version: Optional[str] = None, + debug: Optional[DebugRunContext] = None, decision_names: Optional[List[str]] = None, explain: bool = False, folder_key: Optional[str] = None, @@ -71,10 +79,13 @@ def run( """Run a business rule against one input. Args: - name: The name of the business rule deployed to Orchestrator. + name: The name of the business rule. input: The input to run, keyed by the rule's input names. Declared inputs absent from it bind to null. version: The rule version to run; defaults to the active version. + Sent with debug runs too when given. + debug: Run the undeployed rule from a Studio project instead of the + deployed rule. decision_names: The decisions to evaluate; defaults to the whole model. explain: Whether to record condition-level explanations in the trace. folder_key: The key of the folder to run in. @@ -85,10 +96,12 @@ def run( trace_context: The trace to file the run's spans under. Defaults to the ambient trace: ``UIPATH_TRACE_ID`` and the current span. - A folder is required: ``folder_key``, ``folder_path`` or - ``organization_unit_id``, with ``explain=True`` needing a folder key (from - ``folder_key`` or ``folder_path``). ``folder_key`` and ``folder_path`` are - exclusive. When the caller gives no folder at all, it falls back to + The folder can be given as ``folder_key`` (sent as is), ``folder_path`` + (looked up and sent as its key) or ``organization_unit_id`` (sent as is); + ``folder_key`` and ``folder_path`` are exclusive. A deployed rule needs one + of them; a debug run by project needs none; a debug run by job lineage + needs ``organization_unit_id``. ``explain=True`` always needs a folder key. + When the caller gives no folder at all, it falls back to ``UIPATH_FOLDER_KEY`` and then ``UIPATH_FOLDER_PATH``; an explicit folder is never replaced by the environment's. A ``businessRule`` binding can remap ``name`` and the folder per environment; its folder then replaces @@ -115,6 +128,15 @@ def run( ) for decision in result.decisions: print(decision.decision_name, decision.outputs) + + # An undeployed rule in a Studio project + from uipath.platform.business_rules import DebugRunContext + + result = client.business_rules.run( + "Loan Pricing", + {"creditScore": 740}, + debug=DebugRunContext(project_id="0a1b2c3d-...", file_name="Loan.dmn"), + ) ``` """ name, folder_key, folder_path = self._apply_binding( @@ -127,7 +149,14 @@ def run( if path: key = self._folders_service.retrieve_folder_key(path) mode, spec = self._run_spec( - name, input, version, decision_names, explain, key, organization_unit_id + name, + input, + version, + debug, + decision_names, + explain, + key, + organization_unit_id, ) response = self.request( spec.method, @@ -145,6 +174,7 @@ async def run_async( input: Dict[str, Any], *, version: Optional[str] = None, + debug: Optional[DebugRunContext] = None, decision_names: Optional[List[str]] = None, explain: bool = False, folder_key: Optional[str] = None, @@ -155,10 +185,13 @@ async def run_async( """Asynchronously run a business rule against one input. Args: - name: The name of the business rule deployed to Orchestrator. + name: The name of the business rule. input: The input to run, keyed by the rule's input names. Declared inputs absent from it bind to null. version: The rule version to run; defaults to the active version. + Sent with debug runs too when given. + debug: Run the undeployed rule from a Studio project instead of the + deployed rule. decision_names: The decisions to evaluate; defaults to the whole model. explain: Whether to record condition-level explanations in the trace. folder_key: The key of the folder to run in. @@ -187,7 +220,14 @@ async def run_async( if path: key = await self._folders_service.retrieve_folder_key_async(path) mode, spec = self._run_spec( - name, input, version, decision_names, explain, key, organization_unit_id + name, + input, + version, + debug, + decision_names, + explain, + key, + organization_unit_id, ) response = await self.request_async( spec.method, @@ -245,6 +285,7 @@ def _run_spec( name: str, input: Dict[str, Any], version: Optional[str], + debug: Optional[DebugRunContext], decision_names: Optional[List[str]], explain: bool, folder_key: Optional[str], @@ -252,6 +293,17 @@ def _run_spec( ) -> Tuple[RunMode, RequestSpec]: if explain and not folder_key: raise _missing_folder_key() + if debug is not None: + return RunMode.DEBUG, self._debug_spec( + name, + input, + version, + debug, + decision_names, + explain, + folder_key, + organization_unit_id, + ) if not folder_key and organization_unit_id is None: raise _missing_folder("a deployed business rule") return RunMode.DEPLOYED, self._evaluate_spec( @@ -290,6 +342,80 @@ def _evaluate_spec( headers=_folder_headers(folder_key, organization_unit_id), ) + def _debug_spec( + self, + name: str, + input: Dict[str, Any], + version: Optional[str], + debug: DebugRunContext, + decision_names: Optional[List[str]], + explain: bool, + folder_key: Optional[str], + organization_unit_id: Optional[int], + ) -> RequestSpec: + # The service finds the project one of two ways, documented as + # alternatives: by projectId, read as given, or by businessRuleName from + # the running debug job's lineage. Each request names the project one way + # only; optional values the caller passes are sent as given. + if _present(debug.project_id): + body, headers = _project_mode(debug, organization_unit_id) + else: + body, headers = _job_lineage_mode(name, debug, organization_unit_id) + + body["explain"] = explain + body["inputs"] = [{"id": _SINGLE_INPUT_ID, "data": input}] + if version: + body["version"] = version + if debug.file_name: + body["fileName"] = debug.file_name + if decision_names: + body["decisionNames"] = decision_names + # Folder key: the traces service files the run's spans under it. + if folder_key: + headers[HEADER_FOLDER_KEY] = folder_key + return RequestSpec( + method="POST", + endpoint=_DEBUG_EVALUATE_ENDPOINT, + json=body, + headers=headers, + ) + + +def _project_mode( + debug: DebugRunContext, organization_unit_id: Optional[int] +) -> Tuple[Dict[str, Any], Dict[str, str]]: + # Nothing beyond the project is required. A job key or folder id the caller + # passes anyway is sent as given; UIPATH_JOB_KEY is not added on its own. + headers: Dict[str, str] = {} + job_key = debug.job_key + if job_key and job_key.strip(): + headers[_HEADER_JOB_KEY] = job_key + if organization_unit_id is not None: + headers[_HEADER_ORGANIZATION_UNIT_ID] = str(organization_unit_id) + return {"projectId": debug.project_id}, headers + + +def _job_lineage_mode( + name: str, debug: DebugRunContext, organization_unit_id: Optional[int] +) -> Tuple[Dict[str, Any], Dict[str, str]]: + job_key = debug.job_key or UiPathConfig.job_key + if not job_key or not job_key.strip(): + raise ValueError( + "debug.job_key must be specified when debug.project_id is not: " + "the service resolves the project from the job's lineage. " + "Set it or UIPATH_JOB_KEY." + ) + if organization_unit_id is None: + raise ValueError( + "organization_unit_id must be specified when debug.project_id is not: " + "it is the folder the job's lineage is read under" + ) + headers = { + _HEADER_JOB_KEY: job_key, + _HEADER_ORGANIZATION_UNIT_ID: str(organization_unit_id), + } + return {"businessRuleName": name}, headers + class _TraceHeaders(Dict[str, str]): """Request headers that keep the caller's explicit trace header. @@ -408,12 +534,15 @@ def _single_result( def _to_run_result(mode: RunMode, response: _WireResponse) -> BusinessRuleRunResult: decisions, errors, status = _single_result(response) + deployed = mode == RunMode.DEPLOYED return BusinessRuleRunResult( mode=mode, status=status, decisions=decisions, errors=errors, top_level_error=response.error.code if response.error else None, - business_rule_name=response.business_rule_name, - version=response.version, + business_rule_name=response.business_rule_name if deployed else None, + version=response.version if deployed else None, + project_id=None if deployed else response.project_id, + file_name=None if deployed else response.file_name, ) diff --git a/packages/uipath-platform/src/uipath/platform/business_rules/business_rules.py b/packages/uipath-platform/src/uipath/platform/business_rules/business_rules.py index f63fb97f8..67e48fb8b 100644 --- a/packages/uipath-platform/src/uipath/platform/business_rules/business_rules.py +++ b/packages/uipath-platform/src/uipath/platform/business_rules/business_rules.py @@ -21,6 +21,33 @@ class RunMode(str, Enum): """Which kind of model ran, and so which service endpoint served the run.""" DEPLOYED = "Deployed" + DEBUG = "Debug" + + +class DebugRunContext(BaseModel): + """Run the undeployed rule from a Studio project instead of the deployed one. + + The service finds the project one of two ways: + + - **By project:** set ``project_id`` (and optionally ``file_name``). Nothing + else is required; the project is read as given. + - **By job lineage:** leave ``project_id`` unset. The service finds the project + from the running debug job and checks the rule name against it, so it needs + ``job_key`` (defaults to ``UIPATH_JOB_KEY``) and ``run()``'s + ``organization_unit_id``, the job's numeric folder id. + """ + + project_id: Optional[str] = Field( + default=None, description="Project mode: the Studio project holding the rule." + ) + file_name: Optional[str] = Field( + default=None, + description="The rule file in the project; defaults to the first one.", + ) + job_key: Optional[str] = Field( + default=None, + description="The running debug job: required by job lineage (defaults to UIPATH_JOB_KEY), optional by project.", + ) class TraceContext(BaseModel): @@ -110,10 +137,17 @@ class BusinessRuleRunResult(BaseModel): description="The request-level error code (e.g. BATCH_TIMEOUT), if one was reported.", ) business_rule_name: Optional[str] = Field( - default=None, description="The deployed rule that ran." + default=None, description="The deployed rule that ran. Deployed mode only." ) version: Optional[str] = Field( - default=None, description="The rule version that ran." + default=None, description="The rule version that ran. Deployed mode only." + ) + project_id: Optional[str] = Field( + default=None, description="The Studio project read. Debug mode only." + ) + file_name: Optional[str] = Field( + default=None, + description="The rule file actually read, which may differ from the one asked for. Debug mode only.", ) @@ -132,5 +166,7 @@ class _WireResponse(BaseModel): business_rule_name: Optional[str] = Field(default=None, alias="businessRuleName") version: Optional[str] = None + project_id: Optional[str] = Field(default=None, alias="projectId") + file_name: Optional[str] = Field(default=None, alias="fileName") error: Optional[BusinessRuleError] = None results: Optional[List[_WireResult]] = None diff --git a/packages/uipath-platform/tests/services/test_business_rules_service.py b/packages/uipath-platform/tests/services/test_business_rules_service.py index abeb04026..d15c779c6 100644 --- a/packages/uipath-platform/tests/services/test_business_rules_service.py +++ b/packages/uipath-platform/tests/services/test_business_rules_service.py @@ -12,6 +12,7 @@ from uipath.platform.business_rules import ( BusinessRulesService, BusinessRuleStatus, + DebugRunContext, RunMode, TraceContext, ) @@ -26,6 +27,7 @@ FOLDER_KEY = "5f1f1b0e-2b8a-4c1e-9b8e-1a2b3c4d5e6f" TRACEPARENT = "x-uipath-traceparent-id" RULE = "Loan Pricing" +JOB_KEY = "9d8c7b6a-5f4e-3d2c-1b0a-9f8e7d6c5b4a" @pytest.fixture @@ -45,6 +47,7 @@ def service( ) -> BusinessRulesService: monkeypatch.delenv("UIPATH_FOLDER_KEY", raising=False) monkeypatch.delenv("UIPATH_FOLDER_PATH", raising=False) + monkeypatch.delenv("UIPATH_JOB_KEY", raising=False) return BusinessRulesService( config=config, execution_context=execution_context, @@ -57,6 +60,11 @@ def evaluate_url(base_url: str, org: str, tenant: str) -> str: return f"{base_url}{org}{tenant}/businessrules_/v1/business-rules/evaluate" +@pytest.fixture +def debug_url(base_url: str, org: str, tenant: str) -> str: + return f"{base_url}{org}{tenant}/businessrules_/v1/business-rules/debug/evaluate" + + def _response(results: list[dict[str, Any]], **extra: Any) -> dict[str, Any]: return { "hasPartialSuccess": False, @@ -838,3 +846,284 @@ def test_other_rules_are_not_overridden( request = httpx_mock.get_request() assert request is not None assert json.loads(request.content)["businessRuleName"] == "Risk Tier" + + +class TestDebug: + def test_by_project_id( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response( + url=debug_url, + method="POST", + json=_response( + _one_decision(x=1), + projectId="proj-1", + fileName="Rules/Loan.dmn", + traceId="abc123", + ), + ) + + result = service.run( + RULE, + {"a": 1}, + debug=DebugRunContext(project_id="proj-1", file_name="loan.dmn"), + ) + + assert result.mode == RunMode.DEBUG + assert result.status == BusinessRuleStatus.SUCCESS + assert result.project_id == "proj-1" + assert result.file_name == "Rules/Loan.dmn" + assert result.business_rule_name is None + assert result.version is None + + request = httpx_mock.get_request() + assert request is not None + assert json.loads(request.content) == { + "projectId": "proj-1", + "fileName": "loan.dmn", + "explain": False, + "inputs": [{"id": "input-1", "data": {"a": 1}}], + } + assert "x-uipath-jobkey" not in request.headers + assert "x-uipath-organizationunitid" not in request.headers + assert HEADER_FOLDER_KEY not in request.headers + + def test_by_job_lineage_sends_job_and_folder_headers( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run( + RULE, + {}, + debug=DebugRunContext(job_key=JOB_KEY), + organization_unit_id=42, + folder_key=FOLDER_KEY, + ) + + request = httpx_mock.get_request() + assert request is not None + body = json.loads(request.content) + assert body["businessRuleName"] == RULE + assert "projectId" not in body + assert request.headers["x-uipath-jobkey"] == JOB_KEY + assert request.headers["x-uipath-organizationunitid"] == "42" + assert request.headers[HEADER_FOLDER_KEY] == FOLDER_KEY + + def test_job_key_defaults_from_env( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_JOB_KEY", JOB_KEY) + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run(RULE, {}, debug=DebugRunContext(), organization_unit_id=42) + + request = httpx_mock.get_request() + assert request is not None + assert request.headers["x-uipath-jobkey"] == JOB_KEY + + def test_project_mode_sends_no_job_headers_even_with_env_job_key( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("UIPATH_JOB_KEY", JOB_KEY) + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run(RULE, {}, debug=DebugRunContext(project_id="proj-1")) + + request = httpx_mock.get_request() + assert request is not None + body = json.loads(request.content) + assert body["projectId"] == "proj-1" + assert "businessRuleName" not in body + assert "x-uipath-jobkey" not in request.headers + assert "x-uipath-organizationunitid" not in request.headers + + def test_project_mode_sends_optional_job_key_and_folder_id_as_given( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run( + RULE, + {}, + debug=DebugRunContext(project_id="proj-1", job_key=JOB_KEY), + organization_unit_id=42, + ) + + request = httpx_mock.get_request() + assert request is not None + body = json.loads(request.content) + assert body["projectId"] == "proj-1" + assert "businessRuleName" not in body + assert request.headers["x-uipath-jobkey"] == JOB_KEY + assert request.headers["x-uipath-organizationunitid"] == "42" + + def test_project_mode_needs_no_folder( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run(RULE, {}, debug=DebugRunContext(project_id="proj-1")) + + request = httpx_mock.get_request() + assert request is not None + assert HEADER_FOLDER_KEY not in request.headers + assert "x-uipath-organizationunitid" not in request.headers + + def test_explain_requires_a_folder(self, service: BusinessRulesService) -> None: + debug = DebugRunContext(project_id="proj-1") + + with pytest.raises(ValueError, match="explain=True"): + service.run(RULE, {}, debug=debug, explain=True) + + def test_explain_sends_resolved_folder_key( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + folders_service: Mock, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run( + RULE, + {}, + debug=DebugRunContext(project_id="proj-1"), + explain=True, + folder_path="Finance", + ) + + folders_service.retrieve_folder_key.assert_called_once_with("Finance") + request = httpx_mock.get_request() + assert request is not None + assert request.headers[HEADER_FOLDER_KEY] == FOLDER_KEY + + def test_job_lineage_requires_job_key(self, service: BusinessRulesService) -> None: + with pytest.raises(ValueError, match="job_key"): + service.run(RULE, {}, debug=DebugRunContext(), organization_unit_id=42) + + def test_job_lineage_requires_organization_unit( + self, service: BusinessRulesService + ) -> None: + debug = DebugRunContext(job_key=JOB_KEY) + + with pytest.raises(ValueError, match="organization_unit_id"): + service.run(RULE, {}, debug=debug) + + def test_sends_version_with_debug_when_given( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run( + RULE, {}, version="1.0.0", debug=DebugRunContext(project_id="proj-1") + ) + + request = httpx_mock.get_request() + assert request is not None + assert json.loads(request.content)["version"] == "1.0.0" + + def test_explain_needs_a_folder_key_in_job_lineage_mode( + self, httpx_mock: HTTPXMock, service: BusinessRulesService + ) -> None: + with pytest.raises(ValueError, match="folder key is required for explain"): + service.run( + RULE, + {}, + debug=DebugRunContext(job_key=JOB_KEY), + organization_unit_id=42, + explain=True, + ) + + assert httpx_mock.get_requests() == [] + + def test_rejects_unsafe_rule_name(self, service: BusinessRulesService) -> None: + debug = DebugRunContext(project_id="proj-1") + + with pytest.raises(ValueError, match="name"): + service.run("a/b", {}, debug=debug) + + def test_override_applies_to_debug_runs( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + rule_override: None, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + + service.run( + RULE, + {}, + debug=DebugRunContext(job_key=JOB_KEY), + organization_unit_id=42, + ) + + request = httpx_mock.get_request() + assert request is not None + assert json.loads(request.content)["businessRuleName"] == "Loan Pricing EU" + + def test_explicit_trace_context_on_debug_run( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ambient_span: None, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([])) + explicit = TraceContext( + trace_id=EXPLICIT_TRACE_ID, parent_span_id=EXPLICIT_SPAN_ID + ) + + service.run( + RULE, + {}, + debug=DebugRunContext(project_id="proj-1"), + trace_context=explicit, + ) + + request = httpx_mock.get_request() + assert request is not None + assert ( + request.headers[TRACEPARENT] + == f"00-{EXPLICIT_TRACE_ID}-{EXPLICIT_SPAN_ID}-01" + ) + + async def test_run_async_debug( + self, + httpx_mock: HTTPXMock, + service: BusinessRulesService, + debug_url: str, + ) -> None: + httpx_mock.add_response(url=debug_url, json=_response([], projectId="proj-1")) + + result = await service.run_async( + RULE, {}, debug=DebugRunContext(project_id="proj-1") + ) + + assert result.mode == RunMode.DEBUG + assert result.project_id == "proj-1" + assert result.status == BusinessRuleStatus.ALL_FAILED diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index fee1df2ad..7d91df83e 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "." } dependencies = [ { name = "anyio" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index c43717048..d00f06ff7 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },