diff --git a/src/nomotic/runtime.py b/src/nomotic/runtime.py index 6ffe765..54785e2 100644 --- a/src/nomotic/runtime.py +++ b/src/nomotic/runtime.py @@ -2879,6 +2879,24 @@ def _compute_reversibility_ucs_increase( } return increases.get(level, config.irreversible_ucs_increase) + def _run_post_verdict_processing( + self, + action: Action, + context: AgentContext, + verdict: GovernanceVerdict, + ) -> None: + """Run post-verdict listeners and optional lifecycle pipelines.""" + for listener in self._listeners: + listener(verdict) + + # UAHS post-evaluation pipeline (opt-in) + if self._enable_uahs: + self._post_evaluate_uahs(context.agent_id, verdict) + + # Lifecycle event detection + if self.config.enable_lifecycle_hooks: + self._check_lifecycle_transitions(action.agent_id or context.agent_id, context) + def _record_verdict( self, action: Action, context: AgentContext, verdict: GovernanceVerdict, *, tracer: Any = None, derived_thresholds: Any = None, @@ -3077,16 +3095,7 @@ def _record_verdict( self._append_history(context.agent_id, record) context.action_history.append(record) - for listener in self._listeners: - listener(verdict) - - # UAHS post-evaluation pipeline (opt-in) - if self._enable_uahs: - self._post_evaluate_uahs(context.agent_id, verdict) - - # Lifecycle event detection - if self.config.enable_lifecycle_hooks: - self._check_lifecycle_transitions(action.agent_id or context.agent_id, context) + self._run_post_verdict_processing(action, context, verdict) # BehaviorLedger: build and store the complete decision record if self._behavior_ledger is not None and tracer is not None: diff --git a/tests/test_runtime.py b/tests/test_runtime.py index f55e3ee..32021b7 100644 --- a/tests/test_runtime.py +++ b/tests/test_runtime.py @@ -1,6 +1,8 @@ """Tests for the governance runtime — the full pipeline.""" -from nomotic.types import Action, AgentContext, TrustProfile, Verdict +from unittest.mock import MagicMock + +from nomotic.types import Action, AgentContext, GovernanceVerdict, TrustProfile, Verdict from nomotic.runtime import GovernanceRuntime, RuntimeConfig from nomotic.interrupt import InterruptScope @@ -189,3 +191,47 @@ def test_low_trust_triggers_human_override(self): # Trust should be very low by now trust = runtime.get_trust_profile("agent-1").overall_trust assert trust < 0.3 + + def test_run_post_verdict_processing_calls_listener_uahs_and_lifecycle(self): + runtime = GovernanceRuntime() + events = [] + runtime._listeners = [lambda v: events.append(v.action_id)] + runtime._enable_uahs = True + runtime._post_evaluate_uahs = MagicMock() + runtime._check_lifecycle_transitions = MagicMock() + + action = _action("read", target="db") + context = _ctx(agent_id="agent-1") + verdict = GovernanceVerdict( + action_id=action.id, + verdict=Verdict.ALLOW, + ucs=0.9, + reasoning="ok", + ) + + runtime._run_post_verdict_processing(action, context, verdict) + + assert events == [action.id] + runtime._post_evaluate_uahs.assert_called_once_with("agent-1", verdict) + runtime._check_lifecycle_transitions.assert_called_once_with("agent-1", context) + + def test_run_post_verdict_processing_respects_disabled_flags(self): + runtime = GovernanceRuntime(RuntimeConfig(enable_lifecycle_hooks=False)) + runtime._listeners = [] + runtime._enable_uahs = False + runtime._post_evaluate_uahs = MagicMock() + runtime._check_lifecycle_transitions = MagicMock() + + action = Action(agent_id="", action_type="read", target="db") + context = _ctx(agent_id="agent-fallback") + verdict = GovernanceVerdict( + action_id=action.id, + verdict=Verdict.ALLOW, + ucs=0.9, + reasoning="ok", + ) + + runtime._run_post_verdict_processing(action, context, verdict) + + runtime._post_evaluate_uahs.assert_not_called() + runtime._check_lifecycle_transitions.assert_not_called()