Files
praxis/tests/test_guardrail.py
T
Praxis CI 012992c44d feat(P01-03-04): CustomerServiceGuardrail — pluggable ruleset (D-019)
server/guardrails/customer_service.py implements the Guardrail interface with
the v0.1 Customer Service ruleset (RESEARCH.md safety baseline): blocks
legal/financial/medical advice, real-company impersonation, and a debrief
output filter that blocks recommendations the learner advise legal action
('tell the customer to sue them' → blocked_legal with a coaching redirect).
Session-start disclaimer text defined ('This is an AI practice session…').
Selected via PRAXIS_GUARDRAIL=customer_service (default); swaps in for
NoOpGuardrail with no pipeline change (D-019). server/__main__.py loads the
guardrail and logs the disclaimer as the first AI utterance. 9 unit tests
pass (disclaimer, blocks legal/financial/medical, allows normal coaching,
debrief filter blocks 'sue them', debrief allows coaching, swappable with
NoOp).

---ci---
phase: 1
milestone: v0.1
plan: 03
task: 03-04
status: execute
persona: backend-engineer
requirements:
  covered: [REQ-ORCH-02, REQ-NFR-SAFE-01]
---/ci---
2026-08-01 13:12:31 +00:00

114 lines
3.0 KiB
Python

"""Unit tests for the CustomerServiceGuardrail (TASK-03-04)."""
from __future__ import annotations
import asyncio
import pytest
from server.guardrails.customer_service import (
CustomerServiceGuardrail,
DISCLAIMER_TEXT,
)
from server.guardrails.noop import NoOpGuardrail
from server.services.base import Guardrail, GuardrailContext
def test_is_guardrail():
assert isinstance(CustomerServiceGuardrail(), Guardrail)
def test_disclaimer_text_defined():
g = CustomerServiceGuardrail()
assert "AI practice session" in g.session_start_disclaimer
assert "not a real conversation" in g.session_start_disclaimer
def test_blocks_legal_advice():
g = CustomerServiceGuardrail()
async def _run():
return await g.check("You should sue the company in small claims court.")
v = asyncio.run(_run())
assert not v.allowed
assert v.category == "blocked_legal"
def test_blocks_financial_advice():
g = CustomerServiceGuardrail()
async def _run():
return await g.check("You should invest in crypto for retirement.")
v = asyncio.run(_run())
assert not v.allowed
assert v.category == "blocked_financial"
def test_blocks_medical_advice():
g = CustomerServiceGuardrail()
async def _run():
return await g.check("That sounds like a diagnosis; see a doctor.")
v = asyncio.run(_run())
assert not v.allowed
assert v.category == "blocked_medical"
def test_allows_normal_coaching_line():
g = CustomerServiceGuardrail()
async def _run():
return await g.check("You acknowledged the customer's frustration well.")
v = asyncio.run(_run())
assert v.allowed
assert v.category == "ok"
def test_debrief_filter_blocks_sue_them():
"""PLAN.md must-have: guardrail flags a 'sue them' recommendation."""
g = CustomerServiceGuardrail()
ctx = GuardrailContext(role="debrief")
async def _run():
return await g.check("You should tell the customer to sue them.", ctx)
v = asyncio.run(_run())
assert not v.allowed
assert v.category == "blocked_legal"
assert v.filtered_text is not None
def test_debrief_allows_normal_coaching():
g = CustomerServiceGuardrail()
ctx = GuardrailContext(role="debrief")
async def _run():
return await g.check(
"What you did well: you acknowledged the customer's frustration "
"and offered a concrete resolution.",
ctx,
)
v = asyncio.run(_run())
assert v.allowed
assert v.category == "ok"
def test_guardrail_swappable_with_noop():
"""D-019: swapping CustomerServiceGuardrail ↔ NoOpGuardrail requires no
pipeline change (both implement the same interface)."""
cs = CustomerServiceGuardrail()
noop = NoOpGuardrail()
async def _run(g):
return await g.check("anything", GuardrailContext())
v_cs = asyncio.run(_run(cs))
v_noop = asyncio.run(_run(noop))
# Both return a GuardrailVerdict — interface-compatible.
assert hasattr(v_cs, "allowed")
assert hasattr(v_noop, "allowed")