This repository has been archived on 2026-09-12. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
praxis/tests/test_live_assist_guardrail.py
T
Praxis CI 81d43666c7 feat(P01): complete assist core + guardrail phase — v0.1.11 tagged
Phase 1 (Assist Core + Guardrail) complete. 8 slices, 4 waves, 24 tasks.
12 REQs covered (3 ASSIST + 3 NFR + 6 IDEATE). 92 new tests (409 total).
G-049 + G-067 MUSTs resolved. Verify: APPROVE_WITH_NOTES, 5 P1+ flagged.

Live Assist voice loop: shift-bounded sessions, context-binding,
3-layer guardrail (prompt + regex filter + audit log), tap-to-talk
client control, warm WebRTC, reconnect logic, incremental audit write,
PII policy, consent disclosure, mode-conflict enforcement.

---ci---
project: praxis
phase: 1
milestone: v0.5
status: complete
requirements:
  covered: [REQ-ASSIST-01, REQ-ASSIST-02, REQ-ASSIST-03, REQ-NFR-ASSIST-02, REQ-NFR-ASSIST-03, REQ-NFR-ASSIST-04, REQ-IDEATE-01, REQ-IDEATE-02, REQ-IDEATE-03, REQ-IDEATE-05, REQ-IDEATE-08, REQ-IDEATE-09]
  partial: []
---/ci---
2026-08-04 21:19:20 +00:00

172 lines
6.0 KiB
Python

"""Unit tests for the LiveAssistGuardrail (TASK-03-04, REQ-ASSIST-03, REQ-IDEATE-02).
Covers SLICE-03:
- Direct-answer patterns → blocked (retry-eligible)
- Imperative patterns → blocked (retry-eligible)
- False-authority → blocked (no retry — hard violation)
- Impersonation → blocked (no retry — hard violation)
- Coaching questions → allowed (category='coaching')
- Neutral text → allowed (category='neutral')
- CANNED_FALLBACK returned as filtered_text on every block
- GuardrailContext(role='assist') accepted (REQ-IDEATE-02)
- Swappable with CustomerServiceGuardrail (D-019 pluggability)
"""
from __future__ import annotations
import asyncio
import pytest
from server.guardrails.customer_service import CustomerServiceGuardrail
from server.guardrails.live_assist import (
CANNED_FALLBACK,
LiveAssistGuardrail,
RETRY_ELIGIBLE_CATEGORIES,
HARD_VIOLATION_CATEGORIES,
)
from server.services.base import Guardrail, GuardrailContext
def _check(text: str, role: str = "assist"):
g = LiveAssistGuardrail()
return asyncio.run(g.check(text, GuardrailContext(role=role)))
def test_is_guardrail():
assert isinstance(LiveAssistGuardrail(), Guardrail)
def test_role_assist_accepted():
"""REQ-IDEATE-02: GuardrailContext(role='assist') is accepted."""
v = _check("What do you think the customer needs?", role="assist")
assert v.allowed
# ── Direct-answer patterns (retry-eligible) ───────────────────────────────────
@pytest.mark.parametrize("text", [
"You should say sorry to the customer.",
"Tell the customer you'll refund them.",
"The answer is to offer an exchange.",
"Say this: I apologize for the inconvenience.",
"Here's what to say to the customer.",
"What you should say is sorry.",
])
def test_blocks_direct_answer_patterns(text):
v = _check(text)
assert not v.allowed
assert v.category == "blocked_direct_script"
assert v.filtered_text == CANNED_FALLBACK
assert v.category in RETRY_ELIGIBLE_CATEGORIES
# ── Imperative patterns (retry-eligible) ──────────────────────────────────────
@pytest.mark.parametrize("text", [
"Escalate to a supervisor.",
"Offer a refund of $50.",
"Apologize by saying sorry.",
"Give them a full refund.",
"Promise them a replacement.",
])
def test_blocks_imperative_patterns(text):
v = _check(text)
assert not v.allowed
assert v.category == "blocked_imperative"
assert v.filtered_text == CANNED_FALLBACK
assert v.category in RETRY_ELIGIBLE_CATEGORIES
# ── False-authority (hard violation — no retry) ──────────────────────────────
@pytest.mark.parametrize("text", [
"I am your manager.",
"On behalf of the company, I authorize a refund.",
"I can approve a full refund right now.",
"I'm the supervisor here.",
])
def test_blocks_false_authority(text):
v = _check(text)
assert not v.allowed
assert v.category == "blocked_false_authority"
assert v.filtered_text == CANNED_FALLBACK
assert v.category in HARD_VIOLATION_CATEGORIES
assert v.category not in RETRY_ELIGIBLE_CATEGORIES
# ── Impersonation (hard violation — no retry) ─────────────────────────────────
def test_blocks_impersonation():
v = _check("I work at Amazon and can process your refund.")
assert not v.allowed
assert v.category == "blocked_impersonation"
assert v.filtered_text == CANNED_FALLBACK
assert v.category in HARD_VIOLATION_CATEGORIES
# ── Coaching questions (allowed) ────────────────────────────────────────────
@pytest.mark.parametrize("text", [
"What do you think the customer needs?",
"How could you acknowledge their frustration?",
"What's your next step here?",
"What might happen if you offer a replacement?",
"Can you think of a way to reframe that?",
"Have you considered asking about their preferred outcome?",
])
def test_allows_coaching_questions(text):
v = _check(text)
assert v.allowed
assert v.category == "coaching"
# ── Neutral text (allowed, not ideal) ────────────────────────────────────────
def test_allows_neutral_text():
v = _check("That's a good approach.")
assert v.allowed
assert v.category == "neutral"
def test_neutral_for_short_acknowledgement():
v = _check("Okay.")
assert v.allowed
assert v.category == "neutral"
# ── session_start_disclaimer (Layer 1) ────────────────────────────────────────
def test_session_start_disclaimer_is_coaching_instruction():
"""The disclaimer is the coaching-mode system prompt (D-066), not spoken audio."""
g = LiveAssistGuardrail()
disclaimer = g.session_start_disclaimer
assert "coach" in disclaimer.lower()
assert "guiding questions" in disclaimer.lower()
assert "never give the answer" in disclaimer.lower()
assert "never claim authority" in disclaimer.lower()
# ── D-019 pluggability ────────────────────────────────────────────────────────
def test_swappable_with_customer_service_guardrail():
"""D-019: both guardrails implement the same interface — swappable."""
live = LiveAssistGuardrail()
cs = CustomerServiceGuardrail()
async def _run(g, text):
return await g.check(text, GuardrailContext(role="assist"))
v_live = asyncio.run(_run(live, "What do you think?"))
v_cs = asyncio.run(_run(cs, "What do you think?"))
# Both return a GuardrailVerdict — interface-compatible.
assert hasattr(v_live, "allowed")
assert hasattr(v_cs, "allowed")