"""Unit tests for the scenario schema + loader (TASK-03-01, TASK-03-02).""" from __future__ import annotations from pathlib import Path import pytest import yaml from server.scenarios.schema import Scenario, ValidationError from server.scenarios.loader import load VALID_SCENARIO_DICT = { "id": "cs_refund_ca_v01", "path": "customer_service", "market": "CA", "language": "en-CA", "title": "Angry customer requesting refund on a damaged product", "difficulty": 1, "failure_mode": "escalates_unresolved", "persona": { "voice_id": "cartesia:some-voice-id", "character": "Customer (Jordan)", }, "setup": { "system_prompt": "You are Jordan, a customer who received a damaged product.", "opening_line": "Hi, I received my order yesterday and the item is cracked.", }, "success_criteria": ["Acknowledged the customer's frustration empathetically"], "common_mistakes": ["Jumping to policy before acknowledging emotion"], "branches": [ { "id": "accept_resolution", "trigger": {"learner_signals": ["empathy", "concrete_resolution"]}, "outcome": "success", "debrief_focus": "What you did well", }, { "id": "escalate", "trigger": {"learner_signals": ["defensive", "policy_first"]}, "outcome": "failure", "failure_mode": "escalates_unresolved", "debrief_focus": "The customer escalated because they felt unheard", }, ], "debrief": { "model": "deepseek-v4-flash:cloud", "mode": "no_think", "prompt_template": "debrief/default", }, } def test_valid_scenario_parses(): s = Scenario.model_validate(VALID_SCENARIO_DICT) assert s.id == "cs_refund_ca_v01" assert s.failure_mode == "escalates_unresolved" assert len(s.branches) == 2 assert s.branch_ids() == ["accept_resolution", "escalate"] def test_invalid_scenario_raises_typed_error(): bad = dict(VALID_SCENARIO_DICT) bad["failure_mode"] = None # required field → ValidationError with pytest.raises(ValidationError): Scenario.model_validate(bad) def test_invalid_branch_outcome_raises(): bad = dict(VALID_SCENARIO_DICT) bad["branches"] = [ { "id": "x", "trigger": {"learner_signals": ["a"]}, "outcome": "not_a_real_outcome", # Literal mismatch "debrief_focus": "f", } ] with pytest.raises(ValidationError): Scenario.model_validate(bad) def test_scenario_branch_by_id(): s = Scenario.model_validate(VALID_SCENARIO_DICT) b = s.branch_by_id("escalate") assert b is not None assert b.outcome == "failure" assert b.failure_mode == "escalates_unresolved" assert s.branch_by_id("nonexistent") is None def test_load_customer_service_refund_scenario(): """TASK-03-02 verification: the real YAML loads and validates.""" s = load("customer_service_refund_ca_v01") assert s.id == "cs_refund_ca_v01" assert s.failure_mode == "escalates_unresolved" assert len(s.branches) == 2 assert s.branch_by_id("accept_resolution") is not None assert s.branch_by_id("escalate") is not None assert s.debrief.model == "deepseek-v4-flash:cloud" assert s.debrief.mode == "no_think"