1010from uipath .platform .common import UiPathSpan , _SpanUtils
1111
1212
13+ @pytest .fixture (autouse = True )
14+ def _clear_agent_id_cache ():
15+ """Isolate the process-global agentId cache between tests."""
16+ from uipath .platform .common ._span_utils import _read_config_agent_id
17+
18+ _read_config_agent_id .cache_clear ()
19+ yield
20+ _read_config_agent_id .cache_clear ()
21+
22+
1323class TestOTelToUiPathSpan :
1424 """OTEL attribute -> top-level UiPathSpan field mapping.
1525
@@ -92,10 +102,11 @@ def test_verbosity_level_omitted_when_unset(self) -> None:
92102class TestReferenceIdResolution :
93103 """`reference_id` resolution chain.
94104
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.
105+ `reference_id` is derived from the span's resolved `agentId` attribute
106+ (which itself goes through `resolve_agent_id()`), falling back to the
107+ `referenceId` attribute. Falsy values (missing / empty string) at each step
108+ fall through to the next source. The `referenceId` fallback exists for
109+ backwards compatibility with older producers that only emit that attribute.
99110 """
100111
101112 @pytest .mark .parametrize (
@@ -105,7 +116,7 @@ class TestReferenceIdResolution:
105116 "env-agent" ,
106117 {"agentId" : "attr-agent" , "referenceId" : "attr-ref" },
107118 "env-agent" ,
108- id = "env-var-wins " ,
119+ id = "env-var-overrides-attr " ,
109120 ),
110121 pytest .param (
111122 None ,
@@ -140,6 +151,10 @@ def test_reference_id_chain(
140151 expected : str | None ,
141152 monkeypatch : pytest .MonkeyPatch ,
142153 ) -> None :
154+ from uipath .platform .common ._span_utils import _read_config_agent_id
155+
156+ _read_config_agent_id .cache_clear ()
157+ monkeypatch .delenv ("PROJECT_KEY" , raising = False )
143158 if env_value is None :
144159 monkeypatch .delenv ("UIPATH_AGENT_ID" , raising = False )
145160 else :
@@ -166,6 +181,129 @@ def test_reference_id_chain(
166181 assert uipath_span .reference_id == expected
167182
168183
184+ class TestAgentIdResolution :
185+ """`agentId` span attribute resolution via `resolve_agent_id()`.
186+
187+ Priority: `uipath.json#agentId` (cached, read once per process) >
188+ `UIPATH_AGENT_ID` env var > the legacy `PROJECT_KEY` env var injected by
189+ the executor. When no source is present the `agentId` attribute is omitted
190+ entirely.
191+ """
192+
193+ @staticmethod
194+ def _make_span () -> Mock :
195+ mock_span = Mock (spec = OTelSpan )
196+ mock_context = SpanContext (
197+ trace_id = 0x123456789ABCDEF0123456789ABCDEF0 ,
198+ span_id = 0x0123456789ABCDEF ,
199+ is_remote = False ,
200+ )
201+ mock_span .get_span_context .return_value = mock_context
202+ mock_span .name = "test-span"
203+ mock_span .parent = None
204+ mock_span .status .status_code = StatusCode .OK
205+ mock_span .attributes = {}
206+ mock_span .events = []
207+ mock_span .links = []
208+ now_ns = int (datetime .now ().timestamp () * 1e9 )
209+ mock_span .start_time = now_ns
210+ mock_span .end_time = now_ns + 1_000_000
211+ return mock_span
212+
213+ @staticmethod
214+ def _resolve (monkeypatch : pytest .MonkeyPatch , tmp_path ) -> object :
215+ from uipath .platform .common ._span_utils import _read_config_agent_id
216+
217+ _read_config_agent_id .cache_clear ()
218+ monkeypatch .delenv ("UIPATH_CONFIG_PATH" , raising = False )
219+ monkeypatch .delenv ("UIPATH_AGENT_ID" , raising = False )
220+ monkeypatch .chdir (tmp_path )
221+ uipath_span = _SpanUtils .otel_span_to_uipath_span (
222+ TestAgentIdResolution ._make_span (), serialize_attributes = False
223+ )
224+ attributes = uipath_span .attributes
225+ assert isinstance (attributes , dict )
226+ return attributes .get ("agentId" )
227+
228+ def test_agent_id_from_uipath_json_wins_over_env (
229+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
230+ ) -> None :
231+ (tmp_path / "uipath.json" ).write_text (json .dumps ({"agentId" : "from-config" }))
232+ monkeypatch .setenv ("PROJECT_KEY" , "from-env" )
233+ assert self ._resolve (monkeypatch , tmp_path ) == "from-config"
234+
235+ def test_agent_id_from_uipath_json_wins_over_agent_id_env (
236+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
237+ ) -> None :
238+ (tmp_path / "uipath.json" ).write_text (json .dumps ({"agentId" : "from-config" }))
239+ monkeypatch .setenv ("UIPATH_AGENT_ID" , "from-agent-env" )
240+ # _resolve clears UIPATH_AGENT_ID, so set it back after the fixture work
241+ # is done via a direct resolve call instead.
242+ from uipath .platform .common ._span_utils import (
243+ _read_config_agent_id ,
244+ resolve_agent_id ,
245+ )
246+
247+ _read_config_agent_id .cache_clear ()
248+ monkeypatch .chdir (tmp_path )
249+ assert resolve_agent_id () == "from-config"
250+
251+ def test_agent_id_env_wins_over_project_key_env (
252+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
253+ ) -> None :
254+ # No uipath.json on disk.
255+ from uipath .platform .common ._span_utils import (
256+ _read_config_agent_id ,
257+ resolve_agent_id ,
258+ )
259+
260+ _read_config_agent_id .cache_clear ()
261+ monkeypatch .chdir (tmp_path )
262+ monkeypatch .setenv ("UIPATH_AGENT_ID" , "from-agent-env" )
263+ monkeypatch .setenv ("PROJECT_KEY" , "from-project-key" )
264+ assert resolve_agent_id () == "from-agent-env"
265+
266+ def test_agent_id_falls_back_to_project_key_env (
267+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
268+ ) -> None :
269+ # No uipath.json on disk.
270+ monkeypatch .setenv ("PROJECT_KEY" , "from-env" )
271+ assert self ._resolve (monkeypatch , tmp_path ) == "from-env"
272+
273+ def test_agent_id_falls_back_when_config_has_no_agent_id (
274+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
275+ ) -> None :
276+ (tmp_path / "uipath.json" ).write_text (json .dumps ({"functions" : {}}))
277+ monkeypatch .setenv ("PROJECT_KEY" , "from-env" )
278+ assert self ._resolve (monkeypatch , tmp_path ) == "from-env"
279+
280+ def test_agent_id_absent_when_no_source (
281+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
282+ ) -> None :
283+ monkeypatch .delenv ("PROJECT_KEY" , raising = False )
284+ assert self ._resolve (monkeypatch , tmp_path ) is None
285+
286+ def test_config_agent_id_is_cached (
287+ self , monkeypatch : pytest .MonkeyPatch , tmp_path
288+ ) -> None :
289+ from uipath .platform .common ._span_utils import _read_config_agent_id
290+
291+ _read_config_agent_id .cache_clear ()
292+ monkeypatch .delenv ("UIPATH_CONFIG_PATH" , raising = False )
293+ monkeypatch .chdir (tmp_path )
294+ config = tmp_path / "uipath.json"
295+
296+ config .write_text (json .dumps ({"agentId" : "first" }))
297+ assert _read_config_agent_id () == "first"
298+
299+ # A later edit is not observed: the value is read once and cached.
300+ config .write_text (json .dumps ({"agentId" : "second" }))
301+ assert _read_config_agent_id () == "first"
302+
303+ _read_config_agent_id .cache_clear ()
304+ assert _read_config_agent_id () == "second"
305+
306+
169307class TestNormalizeIds :
170308 """Tests for OTEL ID normalization functions."""
171309
0 commit comments