11"""Tests for trace-span verbosity / status semantics.
22
3- ``TracesAuditSink`` emits an OpenTelemetry span for every governance
4- hook end and every rule evaluation. The verdict is split into
5- ``evaluator_result`` (what the rule decided, mode-independent) and
6- ``action_applied`` (what actually happened, derived from
7- evaluator_result + mode).
3+ ``TracesAuditSink`` emits an OpenTelemetry span for every rule
4+ evaluation. HOOK_END events are dropped — no hook-summary span. The
5+ verdict is split into ``evaluator_result`` (what the rule decided,
6+ mode-independent) and ``action_applied`` (what actually happened,
7+ derived from evaluator_result + mode).
88
99Mode travels with the event (set by the emitter from its
1010per-instance ``EnforcementMode``) so parallel runtimes running
1717 outcomes (``action_applied`` in ``{AUDIT, HITL}``). HITL is its own
1818 bucket — escalation pauses for human review, it doesn't fail the
1919 run, so it stays Warning even in ENFORCE mode.
20- - Hook spans never set Status, regardless of mode or final_action.
21- They're summary containers; severity belongs on the per-rule span.
2220- ``ALLOW`` / ``NONE`` results leave verbosityLevel unset (consumers
2321 apply their default) and never call set_status.
2422"""
3129from uipath .core .governance import EnforcementMode
3230
3331from uipath .runtime .governance ._audit .base import AuditEvent , EventType
34- from uipath .runtime .governance ._audit .traces import TracesAuditSink
32+ from uipath .runtime .governance ._audit .traces import (
33+ SPAN_TYPE_GOVERNANCE ,
34+ UIPATH_SOURCE ,
35+ TracesAuditSink ,
36+ )
3537
3638
3739@pytest .fixture
@@ -89,7 +91,7 @@ def _span_attrs(span: MagicMock) -> dict[str, object]:
8991
9092
9193# ---------------------------------------------------------------------------
92- # Hook span — never marked ERROR
94+ # Hook span — dropped; no span is emitted for HOOK_END
9395# ---------------------------------------------------------------------------
9496
9597
@@ -103,16 +105,46 @@ def _span_attrs(span: MagicMock) -> dict[str, object]:
103105 ("allow" , EnforcementMode .AUDIT ),
104106 ],
105107)
106- def test_hook_span_never_sets_error (
108+ def test_hook_end_emits_no_span (
107109 captured_span : MagicMock , final_action : str , mode : EnforcementMode
108110) -> None :
109- """Hook spans are summary containers — they never carry an ERROR Status."""
111+ """HOOK_END events are dropped — the sink emits spans per rule only.
112+
113+ No span is started and the counter stays at zero regardless of the
114+ hook's final_action or mode.
115+ """
110116 sink = TracesAuditSink ()
111117 sink .emit (_hook_event (final_action = final_action , mode = mode ))
112- assert not captured_span .set_status .called , (
113- f"Hook span should never set_status; called with "
114- f"final_action={ final_action !r} , mode={ mode !r} "
115- )
118+
119+ # The tracer is never asked to start a span for HOOK_END — the drop
120+ # happens in ``emit`` before any span work.
121+ tracer = sink ._get_tracer ()
122+ tracer .start_as_current_span .assert_not_called ()
123+ assert not captured_span .set_attribute .called
124+ assert not captured_span .set_status .called
125+ assert sink .spans_created == 0
126+
127+
128+ # ---------------------------------------------------------------------------
129+ # Rule span — OTEL-contract attributes (span type + source)
130+ # ---------------------------------------------------------------------------
131+
132+
133+ def test_rule_span_sets_governance_type_and_source (captured_span : MagicMock ) -> None :
134+ """Governance rule spans carry the governance span type and source.
135+
136+ This is the OTEL-contract change the PR exists to make: spans must no
137+ longer look like agent runs (``agentRun`` / CodedAgents). Guards
138+ against a regression that recreates the span but mis-types/mis-sources
139+ it.
140+ """
141+ sink = TracesAuditSink ()
142+ sink .emit (_rule_event (matched = True , action = "deny" , mode = EnforcementMode .ENFORCE ))
143+
144+ attrs = _span_attrs (captured_span )
145+ assert attrs .get ("type" ) == SPAN_TYPE_GOVERNANCE
146+ assert attrs .get ("span_type" ) == SPAN_TYPE_GOVERNANCE
147+ assert attrs .get ("uipath.source" ) == UIPATH_SOURCE
116148
117149
118150# ---------------------------------------------------------------------------
@@ -324,13 +356,13 @@ def test_emit_is_a_noop_when_tracer_unavailable(
324356 monkeypatch : pytest .MonkeyPatch ,
325357) -> None :
326358 """When ``_get_tracer`` returns None, ``emit`` short-circuits inside
327- both ``_emit_rule_span`` and ``_emit_hook_span`` without touching
328- the tracer. Regression guard for the early-return branch.
359+ ``_emit_rule_span`` without touching the tracer. Regression guard
360+ for the early-return branch.
329361 """
330362 sink = TracesAuditSink ()
331363 monkeypatch .setattr (sink , "_get_tracer" , lambda : None )
332364
333- # Both event types — must not raise, must not create spans.
365+ # Rule events must not raise, must not create spans; HOOK_END is dropped .
334366 sink .emit (_hook_event (final_action = "deny" , mode = EnforcementMode .ENFORCE ))
335367 sink .emit (_rule_event (matched = True , action = "deny" , mode = EnforcementMode .ENFORCE ))
336368 assert sink .spans_created == 0
@@ -353,6 +385,9 @@ def test_spans_created_increments_per_successful_emit(
353385 assert sink .spans_created == 0
354386 sink .emit (_rule_event (matched = True , action = "deny" , mode = EnforcementMode .ENFORCE ))
355387 assert sink .spans_created == 1
388+ # A second rule increments; HOOK_END events are dropped and never do.
389+ sink .emit (_rule_event (matched = True , action = "deny" , mode = EnforcementMode .ENFORCE ))
390+ assert sink .spans_created == 2
356391 sink .emit (_hook_event (final_action = "deny" , mode = EnforcementMode .ENFORCE ))
357392 assert sink .spans_created == 2
358393
@@ -466,7 +501,6 @@ def test_rule_span_exception_is_swallowed(
466501
467502 # Must not raise.
468503 sink .emit (_rule_event (matched = True , action = "deny" , mode = EnforcementMode .ENFORCE ))
469- sink .emit (_hook_event (final_action = "deny" , mode = EnforcementMode .ENFORCE ))
470504
471505 # And no span was successfully created.
472506 assert sink .spans_created == 0
0 commit comments