012992c44d
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---
114 lines
3.0 KiB
Python
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") |