Files
praxis/tests/test_classifier.py
T
Praxis CI fbd6602814 docs(milestone): complete v0.1 foundation
---ci---
phase: 0
milestone: v0.1
status: complete
---/ci---
2026-08-01 13:32:48 +00:00

125 lines
4.5 KiB
Python

"""Tests for interruptibility (TASK-03-05) + branch classifier (TASK-03-06)."""
from __future__ import annotations
import asyncio
from types import SimpleNamespace
import pytest
from server.interruptibility import pipeline_allows_interruptions
from server.scenarios.classifier import (
classify_branch,
classify_branch_sync_heuristic,
_parse_branch,
)
from server.scenarios.loader import load
# ─── TASK-03-05: interruptibility ────────────────────────────────────────────
def test_pipeline_allows_interruptions_flag():
"""The pipeline task must be configured with allow_interruptions=True (D-008)."""
task = SimpleNamespace(params=SimpleNamespace(allow_interruptions=True))
assert pipeline_allows_interruptions(task) is True
def test_pipeline_allows_interruptions_false_flag():
task = SimpleNamespace(params=SimpleNamespace(allow_interruptions=False))
assert pipeline_allows_interruptions(task) is False
def test_pipeline_allows_interruptions_missing_params():
task = SimpleNamespace()
assert pipeline_allows_interruptions(task) is False
# ─── TASK-03-06: branch classifier ───────────────────────────────────────────
def test_heuristic_classifier_accept():
"""A scripted empathetic transcript → accept_resolution."""
scenario = load("customer_service_refund_ca_v01")
turns = [
"I'm really sorry you're frustrated. I can offer a full refund right now.",
"Let me confirm the next steps for you.",
]
branch_id = classify_branch_sync_heuristic(scenario, turns)
assert branch_id == "accept_resolution"
def test_heuristic_classifier_escalate():
"""A scripted defensive transcript → escalate."""
scenario = load("customer_service_refund_ca_v01")
turns = [
"Well, our policy says we don't do refunds after 30 days.",
"That's just how it works, I can't help you.",
]
branch_id = classify_branch_sync_heuristic(scenario, turns)
assert branch_id == "escalate"
def test_parse_branch_valid_json():
scenario = load("customer_service_refund_ca_v01")
text = '{"branch_id": "accept_resolution", "reason": "empathy shown"}'
bid, reason = _parse_branch(text, scenario)
assert bid == "accept_resolution"
assert "empathy" in reason
def test_parse_branch_code_fenced_json():
scenario = load("customer_service_refund_ca_v01")
text = '```json\n{"branch_id": "escalate", "reason": "defensive"}\n```'
bid, reason = _parse_branch(text, scenario)
assert bid == "escalate"
def test_parse_branch_unknown_id_falls_back():
scenario = load("customer_service_refund_ca_v01")
text = '{"branch_id": "not_real", "reason": "x"}'
bid, reason = _parse_branch(text, scenario)
# Falls back to the first branch.
assert bid in scenario.branch_ids()
assert "fallback" in reason
def test_parse_branch_malformed_json_scans_for_id():
scenario = load("customer_service_refund_ca_v01")
text = "The learner matches the escalate branch."
bid, reason = _parse_branch(text, scenario)
assert bid == "escalate"
assert "fallback" in reason
class _FakeLLM:
"""A fake LLMProvider for the classifier test (no real API call)."""
debrief_model = "deepseek-v4-flash:cloud"
roleplay_model = "gemma4:cloud"
async def chat_full(self, messages, *, model=None, no_think=False):
return (
'{"branch_id": "accept_resolution", "reason": "empathy + concrete_resolution"}',
{"output_tokens": 10},
)
def test_classify_branch_with_fake_llm():
"""The async classifier returns the LLM's branch verdict (R7, offline)."""
scenario = load("customer_service_refund_ca_v01")
turns = ["I'm sorry, I can offer a refund."]
bid, reason = asyncio.run(classify_branch(_FakeLLM(), scenario, turns))
assert bid == "accept_resolution"
assert "empathy" in reason
def test_classifier_runs_offline_from_voice_loop():
"""D-P1-05: the classifier is a one-shot end-of-session call, not per-turn."""
# This is a structural assertion: classify_branch takes the full turns list,
# not a single turn — confirming it runs at session end, not on the
# latency-critical voice path.
scenario = load("customer_service_refund_ca_v01")
turns = ["turn 1", "turn 2", "turn 3"]
bid = classify_branch_sync_heuristic(scenario, turns)
assert bid in scenario.branch_ids()