88from opentelemetry .trace import SpanContext , StatusCode
99
1010from uipath .platform .common import UiPathSpan , _SpanUtils
11+ from uipath .platform .common .constants import (
12+ ENV_PROJECT_KEY ,
13+ ENV_UIPATH_AGENT_ID ,
14+ ENV_UIPATH_PROJECT_ID ,
15+ )
16+
17+
18+ @pytest .fixture (autouse = True )
19+ def _clear_id_cache ():
20+ """Isolate the process-global id cache between tests."""
21+ from uipath .platform .common ._span_utils import _read_config_id
22+
23+ _read_config_id .cache_clear ()
24+ yield
25+ _read_config_id .cache_clear ()
1126
1227
1328class TestOTelToUiPathSpan :
@@ -92,10 +107,11 @@ def test_verbosity_level_omitted_when_unset(self) -> None:
92107class TestReferenceIdResolution :
93108 """`reference_id` resolution chain.
94109
95- Priority: `UIPATH_AGENT_ID` env var > `agentId` attribute > `referenceId`
96- attribute. Falsy values (missing / empty string) at each step fall through
97- to the next source. The `referenceId` fallback exists for backwards
98- compatibility with older producers that only emit that attribute.
110+ `reference_id` is derived from the span's resolved `agentId` attribute
111+ (which itself goes through `resolve_id()`), falling back to the
112+ `referenceId` attribute. Falsy values (missing / empty string) at each step
113+ fall through to the next source. The `referenceId` fallback exists for
114+ backwards compatibility with older producers that only emit that attribute.
99115 """
100116
101117 @pytest .mark .parametrize (
@@ -105,7 +121,7 @@ class TestReferenceIdResolution:
105121 "env-agent" ,
106122 {"agentId" : "attr-agent" , "referenceId" : "attr-ref" },
107123 "env-agent" ,
108- id = "env-var-wins " ,
124+ id = "env-var-overrides-attr " ,
109125 ),
110126 pytest .param (
111127 None ,
@@ -140,10 +156,15 @@ def test_reference_id_chain(
140156 expected : str | None ,
141157 monkeypatch : pytest .MonkeyPatch ,
142158 ) -> None :
159+ from uipath .platform .common ._span_utils import _read_config_id
160+
161+ _read_config_id .cache_clear ()
162+ monkeypatch .delenv (ENV_UIPATH_AGENT_ID , raising = False )
163+ monkeypatch .delenv (ENV_UIPATH_PROJECT_ID , raising = False )
143164 if env_value is None :
144- monkeypatch .delenv ("UIPATH_AGENT_ID" , raising = False )
165+ monkeypatch .delenv (ENV_PROJECT_KEY , raising = False )
145166 else :
146- monkeypatch .setenv ("UIPATH_AGENT_ID" , env_value )
167+ monkeypatch .setenv (ENV_PROJECT_KEY , env_value )
147168
148169 mock_span = Mock (spec = OTelSpan )
149170 mock_context = SpanContext (
@@ -166,6 +187,115 @@ def test_reference_id_chain(
166187 assert uipath_span .reference_id == expected
167188
168189
190+ class TestAgentIdResolution :
191+ """`agentId` span attribute resolution via `resolve_id()`.
192+
193+ Priority: `uipath.json#id` (cached, read once per process) > `UIPATH_AGENT_ID`
194+ / `UIPATH_PROJECT_ID` > the legacy `PROJECT_KEY` env var injected by the
195+ executor at runtime. When no source is present the `agentId` attribute is
196+ omitted entirely.
197+ """
198+
199+ @staticmethod
200+ def _make_span () -> Mock :
201+ mock_span = Mock (spec = OTelSpan )
202+ mock_context = SpanContext (
203+ trace_id = 0x123456789ABCDEF0123456789ABCDEF0 ,
204+ span_id = 0x0123456789ABCDEF ,
205+ is_remote = False ,
206+ )
207+ mock_span .get_span_context .return_value = mock_context
208+ mock_span .name = "test-span"
209+ mock_span .parent = None
210+ mock_span .status .status_code = StatusCode .OK
211+ mock_span .attributes = {}
212+ mock_span .events = []
213+ mock_span .links = []
214+ now_ns = int (datetime .now ().timestamp () * 1e9 )
215+ mock_span .start_time = now_ns
216+ mock_span .end_time = now_ns + 1_000_000
217+ return mock_span
218+
219+ @staticmethod
220+ def _resolve (monkeypatch : pytest .MonkeyPatch , tmp_path ) -> object :
221+ from uipath .platform .common ._span_utils import _read_config_id
222+
223+ _read_config_id .cache_clear ()
224+ monkeypatch .delenv ("UIPATH_CONFIG_PATH" , raising = False )
225+ monkeypatch .delenv (ENV_UIPATH_AGENT_ID , raising = False )
226+ monkeypatch .delenv (ENV_UIPATH_PROJECT_ID , raising = False )
227+ monkeypatch .chdir (tmp_path )
228+ uipath_span = _SpanUtils .otel_span_to_uipath_span (
229+ TestAgentIdResolution ._make_span (), serialize_attributes = False
230+ )
231+ attributes = uipath_span .attributes
232+ assert isinstance (attributes , dict )
233+ return attributes .get ("agentId" )
234+
235+ def test_agent_id_from_uipath_json_wins_over_env (
236+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
237+ ) -> None :
238+ (tmp_path / "uipath.json" ).write_text (
239+ json .dumps ({"id" : "00000000-0000-0000-0000-000000000001" })
240+ )
241+ monkeypatch .setenv (ENV_PROJECT_KEY , "from-env" )
242+ assert (
243+ self ._resolve (monkeypatch , tmp_path )
244+ == "00000000-0000-0000-0000-000000000001"
245+ )
246+
247+ def test_agent_id_falls_back_to_project_key (
248+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
249+ ) -> None :
250+ # No uipath.json on disk.
251+ monkeypatch .setenv (ENV_PROJECT_KEY , "from-env" )
252+ assert self ._resolve (monkeypatch , tmp_path ) == "from-env"
253+
254+ def test_agent_id_falls_back_when_config_has_no_id (
255+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
256+ ) -> None :
257+ (tmp_path / "uipath.json" ).write_text (json .dumps ({"functions" : {}}))
258+ monkeypatch .setenv (ENV_PROJECT_KEY , "from-env" )
259+ assert self ._resolve (monkeypatch , tmp_path ) == "from-env"
260+
261+ def test_agent_id_absent_when_no_source (
262+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
263+ ) -> None :
264+ monkeypatch .delenv (ENV_PROJECT_KEY , raising = False )
265+ assert self ._resolve (monkeypatch , tmp_path ) is None
266+
267+ def test_non_guid_config_id_is_ignored_and_falls_back (
268+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
269+ ) -> None :
270+ # A malformed (non-GUID) id must not reach ReferenceId; fall back to env.
271+ (tmp_path / "uipath.json" ).write_text (json .dumps ({"id" : "not-a-guid" }))
272+ monkeypatch .setenv (ENV_PROJECT_KEY , "from-env" )
273+ assert self ._resolve (monkeypatch , tmp_path ) == "from-env"
274+
275+ def test_config_id_is_cached (
276+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
277+ ) -> None :
278+ from uipath .platform .common ._span_utils import _read_config_id
279+
280+ first = "00000000-0000-0000-0000-000000000001"
281+ second = "00000000-0000-0000-0000-000000000002"
282+
283+ _read_config_id .cache_clear ()
284+ monkeypatch .delenv ("UIPATH_CONFIG_PATH" , raising = False )
285+ monkeypatch .chdir (tmp_path )
286+ config = tmp_path / "uipath.json"
287+
288+ config .write_text (json .dumps ({"id" : first }))
289+ assert _read_config_id () == first
290+
291+ # A later edit is not observed: the value is read once and cached.
292+ config .write_text (json .dumps ({"id" : second }))
293+ assert _read_config_id () == first
294+
295+ _read_config_id .cache_clear ()
296+ assert _read_config_id () == second
297+
298+
169299class TestNormalizeIds :
170300 """Tests for OTEL ID normalization functions."""
171301
0 commit comments