"""Testes do contrato de saída do rail OOS. O `label` ("IN_SCOPE"/"OUT_OF_SCOPE") saiu do prompt em 2026-08-05: nenhum consumidor de produção o lia (a decisão usa `allowed` + `code`, fixado no pipeline) e ele era gerado a cada turno. O `reason` passou a vir VAZIO quando `allowed=true`, como no FRASEOLOGIA — descrever por que algo está DENTRO do escopo não é lido por ninguém. """ from __future__ import annotations import os import pytest os.environ.setdefault("USE_MOCK_LLM", "true") from agent_framework.guardrails.calibrated.llm_rails import out_of_scope from agent_framework.guardrails.calibrated.prompts.out_of_scope import build_oos_prompt @pytest.fixture(autouse=True) def _force_mock(monkeypatch): monkeypatch.setenv("USE_MOCK_LLM", "true") def test_prompt_oos_nao_pede_mais_label(): prompt = build_oos_prompt("Posso ajudar com sua fatura?") assert '"label"' not in prompt assert "IN_SCOPE" not in prompt.split("Responda APENAS JSON valido:")[-1] def test_prompt_oos_exige_reason_vazio_quando_allowed(): prompt = build_oos_prompt("Posso ajudar com sua fatura?") contrato = prompt.split("Responda APENAS JSON valido:")[-1] assert "se allowed=true: string vazia" in contrato # e os exemplos de IN_SCOPE seguem a mesma regra assert '{"allowed": true, "reason": ""}' in prompt def test_oos_rail_devolve_shape_esperado(): r = out_of_scope("Posso te ajudar com mais alguma dúvida sobre a fatura?") assert r.code == "OOS" assert r.mechanism == "llm_supervisor" assert isinstance(r.allowed, bool)