CoolFace
Apppublic

pavan1910/ticket-triage-openenv

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
test_env.py427 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Deterministic test suite for the Ticket Triage OpenEnv environment.4Tests the server endpoints, reward logic, grader scoring, and edge cases5WITHOUT needing any LLM API key.6"""7 8import sys9import json10import requests11 12BASE = "http://localhost:7860"13PASS = 014FAIL = 015TOTAL = 016 17 18def log(msg, ok=True):19    global PASS, FAIL, TOTAL20    TOTAL += 121    if ok:22        PASS += 123        print(f"  ✅ {msg}")24    else:25        FAIL += 126        print(f"  ❌ {msg}")27 28 29def reset(task_id):30    r = requests.post(f"{BASE}/stateful/reset", json={"task_id": task_id}, timeout=10)31    r.raise_for_status()32    return r.json()33 34 35def step(action_type, ticket_id="", payload=""):36    r = requests.post(f"{BASE}/stateful/step", json={37        "action": {"action_type": action_type, "ticket_id": ticket_id, "payload": payload}38    }, timeout=10)39    r.raise_for_status()40    return r.json()41 42 43def state():44    r = requests.get(f"{BASE}/stateful/state", timeout=10)45    r.raise_for_status()46    return r.json()47 48 49# ═══════════════════════════════════════════════════════════════════════════════50# TEST 1: Health & Root Endpoints51# ═══════════════════════════════════════════════════════════════════════════════52 53def test_health_endpoints():54    print("\n🔬 Test 1: Health & Root Endpoints")55 56    r = requests.get(f"{BASE}/health")57    log(f"GET /health → {r.status_code}", r.status_code == 200 and r.json()["status"] == "healthy")58 59    r = requests.get(f"{BASE}/")60    data = r.json()61    log(f"GET / → {data.get('environment')}", data.get("environment") == "ticket-triage")62 63 64# ═══════════════════════════════════════════════════════════════════════════════65# TEST 2: Reset Endpoint (OpenEnv format: observation + reward + done)66# ═══════════════════════════════════════════════════════════════════════════════67 68def test_reset():69    print("\n🔬 Test 2: Reset Endpoint")70 71    for task_id, expected_count in [("task_easy", 2), ("task_medium", 3), ("task_hard", 4)]:72        data = reset(task_id)73        obs = data["observation"]74 75        # Verify OpenEnv response format76        has_reward = "reward" in data77        has_done = "done" in data78        log(79            f"Reset {task_id}: {len(obs['tickets'])} tickets, step={obs['step_number']}, score={obs['current_score']}, format_ok={has_reward and has_done}",80            len(obs["tickets"]) == expected_count81            and obs["step_number"] == 082            and obs["current_score"] == 0.083            and obs["pending_count"] == expected_count84            and has_reward85            and has_done86        )87 88    # Invalid task falls back to task_easy89    data = reset("invalid_task")90    log(f"Invalid task fallback: {len(data['observation']['tickets'])} tickets", len(data["observation"]["tickets"]) == 2)91 92 93# ═══════════════════════════════════════════════════════════════════════════════94# TEST 3: Perfect Run — task_easy95# ═══════════════════════════════════════════════════════════════════════════════96 97def test_perfect_easy():98    print("\n🔬 Test 3: Perfect Run — task_easy")99    reset("task_easy")100 101    total_reward = 0.0102 103    # T1: technical, medium, respond, close104    r = step("categorize", "T1", "technical")105    log(f"T1 categorize=technical → reward={r['reward']}", r["reward"] > 0)106    total_reward += r["reward"]107 108    r = step("set_priority", "T1", "medium")109    log(f"T1 priority=medium → reward={r['reward']}", r["reward"] > 0)110    total_reward += r["reward"]111 112    r = step("respond", "T1", "We're sorry about the login issue. Please try resetting your password using the 'Forgot Password' link on the login page. If you still cannot access your account, we can reset it for you.")113    log(f"T1 respond → reward={r['reward']}", r["reward"] > 0)114    total_reward += r["reward"]115 116    r = step("close", "T1", "")117    log(f"T1 close → reward={r['reward']}", r["reward"] > 0)118    total_reward += r["reward"]119 120    # T2: billing, medium, respond, close121    r = step("categorize", "T2", "billing")122    log(f"T2 categorize=billing → reward={r['reward']}", r["reward"] > 0)123    total_reward += r["reward"]124 125    r = step("set_priority", "T2", "medium")126    log(f"T2 priority=medium → reward={r['reward']}", r["reward"] > 0)127    total_reward += r["reward"]128 129    r = step("respond", "T2", "We apologize for the damage to your order #9921. We will process a full refund immediately. No need to return the damaged item.")130    log(f"T2 respond → reward={r['reward']}", r["reward"] > 0)131    total_reward += r["reward"]132 133    r = step("close", "T2", "")134    log(f"T2 close → reward={r['reward']}", r["reward"] > 0)135    total_reward += r["reward"]136    log(f"T2 close done={r['done']}", r["done"] is True)137 138    final_score = r["observation"]["current_score"]139    log(f"Final score: {final_score} (expected ≥ 0.90)", final_score >= 0.90)140    log(f"Total reward: {round(total_reward, 4)}", total_reward > 0.5)141 142 143# ═══════════════════════════════════════════════════════════════════════════════144# TEST 4: Perfect Run — task_medium (with escalation on T5)145# ═══════════════════════════════════════════════════════════════════════════════146 147def test_perfect_medium():148    print("\n🔬 Test 4: Perfect Run — task_medium")149    reset("task_medium")150 151    total_reward = 0.0152 153    # T3: technical, low, respond, close154    for atype, tid, payload, label in [155        ("categorize", "T3", "technical", "T3 cat"),156        ("set_priority", "T3", "low", "T3 pri"),157        ("respond", "T3", "To reset your password, go to the login page and click the 'Forgot Password' link. Enter your email and follow the reset instructions.", "T3 resp"),158        ("close", "T3", "", "T3 close"),159    ]:160        r = step(atype, tid, payload)161        total_reward += r["reward"]162        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)163 164    # T4: shipping, high, respond, close165    for atype, tid, payload, label in [166        ("categorize", "T4", "shipping", "T4 cat"),167        ("set_priority", "T4", "high", "T4 pri"),168        ("respond", "T4", "We apologize about your order #443. We're tracking delivery with tracking number TRACK-8812 and will escalate with the carrier to ensure fast delivery.", "T4 resp"),169        ("close", "T4", "", "T4 close"),170    ]:171        r = step(atype, tid, payload)172        total_reward += r["reward"]173        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)174 175    # T5: billing, high, respond, ESCALATE, close176    for atype, tid, payload, label in [177        ("categorize", "T5", "billing", "T5 cat"),178        ("set_priority", "T5", "high", "T5 pri"),179        ("respond", "T5", "We sincerely apologize for the duplicate charge. We will reverse the extra charge and issue a refund for transaction TXN-001123 immediately.", "T5 resp"),180        ("escalate", "T5", "", "T5 escalate"),181        ("close", "T5", "", "T5 close"),182    ]:183        r = step(atype, tid, payload)184        total_reward += r["reward"]185        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)186 187    log(f"Episode done={r['done']}", r["done"] is True)188    final_score = r["observation"]["current_score"]189    log(f"Final score: {final_score} (expected ≥ 0.85)", final_score >= 0.85)190    log(f"Total reward: {round(total_reward, 4)}", total_reward > 0.5)191 192 193# ═══════════════════════════════════════════════════════════════════════════════194# TEST 5: Perfect Run — task_hard (security no-response, angry empathy)195# ═══════════════════════════════════════════════════════════════════════════════196 197def test_perfect_hard():198    print("\n🔬 Test 5: Perfect Run — task_hard")199    reset("task_hard")200 201    total_reward = 0.0202 203    # T6: security, critical, escalate ONLY (no respond!), then close204    for atype, tid, payload, label in [205        ("categorize", "T6", "security", "T6 cat"),206        ("set_priority", "T6", "critical", "T6 pri"),207        ("escalate", "T6", "", "T6 escalate"),208        ("close", "T6", "", "T6 close"),209    ]:210        r = step(atype, tid, payload)211        total_reward += r["reward"]212        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)213 214    # T7: technical, critical, respond, escalate, close215    for atype, tid, payload, label in [216        ("categorize", "T7", "technical", "T7 cat"),217        ("set_priority", "T7", "critical", "T7 pri"),218        ("respond", "T7", "We're aware of the API v2 error and our engineering team is actively investigating the 500 errors on the /api/v2/orders endpoint. This is our top priority and we'll provide an update shortly.", "T7 resp"),219        ("escalate", "T7", "", "T7 escalate"),220        ("close", "T7", "", "T7 close"),221    ]:222        r = step(atype, tid, payload)223        total_reward += r["reward"]224        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)225 226    # T8: billing, low, respond (upgrade inquiry), close — NO escalation227    for atype, tid, payload, label in [228        ("categorize", "T8", "billing", "T8 cat"),229        ("set_priority", "T8", "low", "T8 pri"),230        ("respond", "T8", "Thank you for your interest in upgrading your plan! Our Enterprise plan includes advanced features like priority support, custom integrations, and dedicated account management. I'd be happy to help you with the upgrade process.", "T8 resp"),231        ("close", "T8", "", "T8 close"),232    ]:233        r = step(atype, tid, payload)234        total_reward += r["reward"]235        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)236 237    # T9: security, critical, escalate ONLY (legal data breach — no respond!), close238    for atype, tid, payload, label in [239        ("categorize", "T9", "security", "T9 cat"),240        ("set_priority", "T9", "critical", "T9 pri"),241        ("escalate", "T9", "", "T9 escalate"),242        ("close", "T9", "", "T9 close"),243    ]:244        r = step(atype, tid, payload)245        total_reward += r["reward"]246        log(f"{label} → reward={r['reward']}", r["reward"] >= 0)247 248    log(f"Episode done={r['done']}", r["done"] is True)249    final_score = r["observation"]["current_score"]250    log(f"Final score: {final_score} (expected ≥ 0.80)", final_score >= 0.80)251    log(f"Total reward: {round(total_reward, 4)}", total_reward > 1.0)252 253 254# ═══════════════════════════════════════════════════════════════════════════════255# TEST 6: Edge Cases256# ═══════════════════════════════════════════════════════════════════════════════257 258def test_edge_cases():259    print("\n🔬 Test 6: Edge Cases")260    reset("task_easy")261 262    # Invalid ticket ID263    r = step("categorize", "TX99", "technical")264    log(f"Invalid ticket ID → reward={r['reward']}", r["reward"] < 0)265 266    # Invalid category267    r = step("categorize", "T1", "unknown_category")268    log(f"Invalid category → reward={r['reward']}", r["reward"] < 0)269 270    # Invalid priority271    r = step("set_priority", "T1", "extreme")272    log(f"Invalid priority → reward={r['reward']}", r["reward"] < 0)273 274    # Response too short275    r = step("respond", "T1", "ok")276    log(f"Short response → reward={r['reward']}", r["reward"] < 0)277 278    # Note (any length is accepted)279    r = step("add_note", "T1", "hi")280    log(f"Short note → reward={r['reward']}", r["reward"] >= 0)281 282    # Noop penalty283    r = step("noop", "", "")284    log(f"Noop → reward={r['reward']}", r["reward"] < 0)285 286    # Unknown action287    r = step("fly_ticket", "T1", "")288    log(f"Unknown action → reward={r['reward']}", r["reward"] < 0)289 290    # Wrong category gives partial credit (not negative)291    r = step("categorize", "T1", "shipping")292    log(f"Wrong category → reward={r['reward']}", r["reward"] > 0)293 294    # Now fix and close properly295    r = step("categorize", "T1", "technical")296    log(f"Correct re-categorize (no double reward) → reward={r['reward']}", r["reward"] >= 0)297 298 299# ═══════════════════════════════════════════════════════════════════════════════300# TEST 7: State Endpoint301# ═══════════════════════════════════════════════════════════════════════════════302 303def test_state_endpoint():304    print("\n🔬 Test 7: State Endpoint")305    reset("task_easy")306    s = state()307    log(f"State: task={s['task_id']}, step={s['step_count']}, done={s['done']}",308        s["task_id"] == "task_easy" and s["step_count"] == 0 and s["done"] is False)309 310    step("categorize", "T1", "technical")311    s = state()312    log(f"After action: step={s['step_count']}, score={s['score']}", s["step_count"] == 1)313 314 315# ═══════════════════════════════════════════════════════════════════════════════316# TEST 8: Episode termination at step limit317# ═══════════════════════════════════════════════════════════════════════════════318 319def test_step_limit():320    print("\n🔬 Test 8: Step Limit Termination")321    reset("task_easy")  # max_steps = 20322 323    for i in range(21):324        r = step("noop", "", "")325        if r["done"]:326            break327 328    log(f"Episode ended at step limit: done={r['done']}", r["done"] is True)329 330 331# ═══════════════════════════════════════════════════════════════════════════════332# TEST 9: Double close / actions on closed ticket333# ═══════════════════════════════════════════════════════════════════════════════334 335def test_double_close():336    print("\n🔬 Test 9: Double Close & Actions After Close")337    reset("task_easy")338 339    step("categorize", "T1", "technical")340    step("set_priority", "T1", "medium")341    step("respond", "T1", "Please reset your password via the forgot password link on your account page.")342    step("close", "T1", "")343 344    r = step("close", "T1", "")345    log(f"Double close → reward={r['reward']}", r["reward"] < 0)346 347 348# ═══════════════════════════════════════════════════════════════════════════════349# TEST 10: OpenEnv Response Format Validation350# ═══════════════════════════════════════════════════════════════════════════════351 352def test_openenv_response_format():353    print("\n🔬 Test 10: OpenEnv Response Format")354 355    # Test reset response format356    # Test the standard OpenEnv /reset endpoint (from create_fastapi_app)357    r = requests.post(f"{BASE}/reset", json={}, timeout=10)358    data = r.json()359    has_observation = "observation" in data360    has_reward = "reward" in data361    has_done = "done" in data362    log(f"OpenEnv /reset response has observation={has_observation}, reward={has_reward}, done={has_done}",363        has_observation and has_reward and has_done)364 365    # Test stateful reset format366    data = reset("task_easy")367    has_observation = "observation" in data368    has_reward = "reward" in data369    has_done = "done" in data370    no_info = "info" not in data371    log(f"Stateful reset has observation={has_observation}, reward={has_reward}, done={has_done}, no_info={no_info}",372        has_observation and has_reward and has_done and no_info)373    log(f"Reset reward is None", data["reward"] is None)374    log(f"Reset done is False", data["done"] is False)375 376    # Test step response format377    r = step("categorize", "T1", "technical")378    has_observation = "observation" in r379    has_reward = "reward" in r380    has_done = "done" in r381    no_info = "info" not in r382    log(f"Step response has observation={has_observation}, reward={has_reward}, done={has_done}, no_info={no_info}",383        has_observation and has_reward and has_done and no_info)384    log(f"Step reward is float", isinstance(r["reward"], (int, float)))385    log(f"Step done is bool", isinstance(r["done"], bool))386 387    # Test health response format388    h = requests.get(f"{BASE}/health").json()389    log(f"Health status is 'healthy'", h["status"] == "healthy")390 391 392# ═══════════════════════════════════════════════════════════════════════════════393# MAIN394# ═══════════════════════════════════════════════════════════════════════════════395 396if __name__ == "__main__":397    print("=" * 60)398    print("  🎫 Ticket Triage OpenEnv — Test Suite")399    print("=" * 60)400 401    try:402        requests.get(f"{BASE}/health", timeout=3)403    except Exception:404        print(f"\n❌ Cannot reach server at {BASE}. Is it running?")405        sys.exit(1)406 407    test_health_endpoints()408    test_reset()409    test_perfect_easy()410    test_perfect_medium()411    test_perfect_hard()412    test_edge_cases()413    test_state_endpoint()414    test_step_limit()415    test_double_close()416    test_openenv_response_format()417 418    print("\n" + "=" * 60)419    print(f"  Results: {PASS} passed / {FAIL} failed / {TOTAL} total")420    if FAIL == 0:421        print("  🎉 ALL TESTS PASSED!")422    else:423        print(f"  ⚠️  {FAIL} test(s) failed")424    print("=" * 60)425 426    sys.exit(0 if FAIL == 0 else 1)427