from pathlib import Path from types import SimpleNamespace import pytest from agent_framework.guardrails.base import RailDecision from agent_framework.guardrails.output_supervisor import OutputSupervisor from agent_framework.guardrails.rail_action import RailAction from agent_framework.observability.code_mapper import create_observability_code_mapper def _settings(**kwargs): defaults = dict( OBSERVABILITY_DEFAULT_MAPPING_ENABLED=True, OBSERVABILITY_DEFAULT_MAPPING_PATH=None, OBSERVABILITY_CODE_MAPPING_ENABLED=False, OBSERVABILITY_CODE_MAPPING_PATH=None, ) defaults.update(kwargs) return SimpleNamespace(**defaults) def test_old_agent_without_mapping_gets_framework_default_registry(): mapper = create_observability_code_mapper(_settings()) assert mapper.map("guardrail.output_supervisor.started") == "GRL.001" assert mapper.map("guardrail.result.block") == "GRL.004" assert mapper.map("guardrail.output_supervisor.completed") == "GRL.009" assert mapper.action_for("REVPREC") == "retry" assert mapper.action_for("CMP") == "retry" assert mapper.action_for("SCO") == "retry" assert mapper.action_for("GND") == "retry" assert mapper.action_for("ATH") == "handover" assert mapper.map("GRL.004") == "GRL.004" def test_agent_overlay_overrides_only_declared_entries(tmp_path: Path): overlay = tmp_path / "observability_mapping.yaml" overlay.write_text( """version: \"2\"\nmappings:\n guardrail.dlex_in:\n label: GRL.004\n aliases: [DLEX_IN]\n guardrail.tox:\n label: GRL.005\n aliases: [TOX]\n""", encoding="utf-8", ) mapper = create_observability_code_mapper(_settings( OBSERVABILITY_CODE_MAPPING_ENABLED=True, OBSERVABILITY_CODE_MAPPING_PATH=str(overlay), )) # Agent-specific contract overrides framework named GRL.DLEX_IN / GRL.TOX. assert mapper.map("guardrail.dlex_in") == "GRL.004" assert mapper.map("DLEX_IN") == "GRL.004" assert mapper.map("TOX") == "GRL.005" # Unrelated framework defaults remain intact. assert mapper.map("guardrail.result.retry") == "GRL.005" assert mapper.action_for("REVPREC") == "retry" assert mapper.map("guardrail.pinj") == "GRL.PINJ" def test_agent_overlay_can_override_default_action(tmp_path: Path): overlay = tmp_path / "observability_mapping.yaml" overlay.write_text( """mappings:\n guardrail.revprec:\n action: handover\n aliases: [REVPREC]\n""", encoding="utf-8", ) mapper = create_observability_code_mapper(_settings( OBSERVABILITY_CODE_MAPPING_ENABLED=True, OBSERVABILITY_CODE_MAPPING_PATH=str(overlay), )) assert mapper.action_for("REVPREC") == "handover" # Other historical defaults are still inherited. assert mapper.action_for("CMP") == "retry" def test_framework_default_carries_legacy_phraseology_rewrite_policy(): mapper = create_observability_code_mapper(_settings()) remediation = mapper.remediation_for("FRASEOLOGIA") assert remediation is not None assert remediation["type"] == "rewrite" assert remediation["max_attempts"] == 1 assert remediation["generation_name"] == "guardrail.fraseologia.rewrite" class _DeniedLegacyRail: code = "REVPREC" async def evaluate(self, text, context): return RailDecision(code=self.code, allowed=False, reason="premature") @pytest.mark.asyncio async def test_old_legacy_rail_keeps_retry_with_new_framework_and_no_agent_mapping(): mapper = create_observability_code_mapper(_settings()) supervisor = OutputSupervisor( rails=[_DeniedLegacyRail()], enable_parallel=False, observability_mapper=mapper, ) decision = await supervisor.evaluate("candidate", {}) assert decision.action == RailAction.RETRY assert decision.results[0].metadata.get("action_source") == "observability_mapping"