Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Comprehensive test suite for Vera Bot HF Space. | |
| Tests all 5 required endpoints with various scenarios. | |
| """ | |
| import requests | |
| import json | |
| import time | |
| from datetime import datetime, timedelta | |
| BASE_URL = "https://DeepikaChintamreddy-vera-bot.hf.space" | |
| TESTS_PASSED = [] | |
| TESTS_FAILED = [] | |
| def test(name, condition, expected=True): | |
| """Test assertion with logging""" | |
| if bool(condition) == bool(expected): | |
| TESTS_PASSED.append(name) | |
| print(f"✅ {name}") | |
| return True | |
| else: | |
| TESTS_FAILED.append(name) | |
| print(f"❌ {name}") | |
| return False | |
| # ============================================================================ | |
| # TEST 1: GET /v1/healthz | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 1: GET /v1/healthz (Liveness + Context Counts)") | |
| print("="*70) | |
| resp = requests.get(f"{BASE_URL}/v1/healthz") | |
| test("healthz returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("healthz has 'status' field", "status" in data) | |
| test("healthz has 'uptime_seconds' field", "uptime_seconds" in data) | |
| test("healthz has 'contexts_loaded' field", "contexts_loaded" in data) | |
| test("status is 'ok'", data.get("status") == "ok") | |
| test("uptime_seconds > 0", data.get("uptime_seconds", 0) > 0) | |
| print(f" Uptime: {data.get('uptime_seconds')}s | Contexts: {data.get('contexts_loaded')}") | |
| # ============================================================================ | |
| # TEST 2: GET /v1/metadata | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 2: GET /v1/metadata (Team Identity)") | |
| print("="*70) | |
| resp = requests.get(f"{BASE_URL}/v1/metadata") | |
| test("metadata returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("metadata has 'team_name'", "team_name" in data) | |
| test("metadata has 'team_members'", "team_members" in data) | |
| test("metadata has 'contact_email'", "contact_email" in data) | |
| test("metadata has 'version'", "version" in data) | |
| test("metadata has 'model'", "model" in data) | |
| test("metadata has 'approach' (description)", "approach" in data) | |
| test("team_name is 'GrowthGenie'", data.get("team_name") == "GrowthGenie") | |
| print(f" Team: {data.get('team_name')}") | |
| print(f" Members: {data.get('team_members')}") | |
| print(f" Model: {data.get('model')}") | |
| # ============================================================================ | |
| # TEST 3: POST /v1/teardown | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 3: POST /v1/teardown (Wipe State)") | |
| print("="*70) | |
| resp = requests.post(f"{BASE_URL}/v1/teardown", json={}) | |
| test("teardown returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("teardown returns 'wiped'", "wiped" in data) | |
| test("wiped is True", data.get("wiped") == True) | |
| # ============================================================================ | |
| # TEST 4: POST /v1/context - Category | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 4: POST /v1/context (Push Category Context)") | |
| print("="*70) | |
| category_ctx = { | |
| "scope": "category", | |
| "context_id": "dentists", | |
| "version": 5, | |
| "payload": { | |
| "category_slug": "dentists", | |
| "voice": {"register": "peer-clinical", "vocab_taboo": ["guaranteed"]}, | |
| "peer_stats": {"median_ctr": 3.0}, | |
| "offer_catalog": [{"title": "Dental Cleaning", "price": 299}] | |
| } | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/context", json=category_ctx) | |
| test("category context returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("context accepted", data.get("accepted") == True) | |
| test("context has ack_id", "ack_id" in data) | |
| test("context has stored_at", "stored_at" in data) | |
| test("ack_id format correct", "ack_dentists" in data.get("ack_id", "")) | |
| print(f" Ack ID: {data.get('ack_id')}") | |
| # ============================================================================ | |
| # TEST 5: POST /v1/context - Merchant | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 5: POST /v1/context (Push Merchant Context)") | |
| print("="*70) | |
| merchant_ctx = { | |
| "scope": "merchant", | |
| "context_id": "m_001", | |
| "version": 5, | |
| "payload": { | |
| "merchant_id": "m_001", | |
| "category_slug": "dentists", | |
| "identity": {"name": "Dr. Meera Dental Clinic", "languages": ["en", "hi"]}, | |
| "performance": {"current_ctr": 2.1, "delta_7d": {"ctr": -0.9}}, | |
| "offers": [{"title": "Dental Cleaning", "price": 299}], | |
| "customer_aggregate": {"high_risk_adult": 124} | |
| } | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/context", json=merchant_ctx) | |
| test("merchant context returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("merchant context accepted", data.get("accepted") == True) | |
| test("merchant ack_id has m_001", "ack_m_001" in data.get("ack_id", "")) | |
| # ============================================================================ | |
| # TEST 6: POST /v1/context - Trigger | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 6: POST /v1/context (Push Trigger Context)") | |
| print("="*70) | |
| trigger_ctx = { | |
| "scope": "trigger", | |
| "context_id": "trg_test_case_1", | |
| "version": 1, | |
| "payload": { | |
| "id": "trg_test_case_1", | |
| "merchant_id": "m_001", | |
| "kind": "research_digest", | |
| "urgency": 3, | |
| "scope": "merchant", | |
| "suppression_key": "trg_test_case_1_v1", | |
| "source": "internal", | |
| "expires_at": "2026-05-10T23:59:00Z", | |
| "payload": { | |
| "digest_title": "JIDA Oct 2026: Fluoride Varnish", | |
| "key_stat": "Fluoride varnish reduces adult cavities by 38%", | |
| "source": "JIDA Oct 2026" | |
| } | |
| } | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/context", json=trigger_ctx) | |
| test("trigger context returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("trigger context accepted", data.get("accepted") == True) | |
| test("trigger ack_id has trg_test", "ack_trg_test" in data.get("ack_id", "")) | |
| # ============================================================================ | |
| # TEST 7: POST /v1/tick - Trigger Composition | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 7: POST /v1/tick (Compose Message from Trigger)") | |
| print("="*70) | |
| tick_body = { | |
| "now": "2026-04-30T12:10:00Z", | |
| "available_triggers": ["trg_test_case_1"] | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/tick", json=tick_body) | |
| test("tick returns 200", resp.status_code == 200) | |
| data = resp.json() | |
| test("tick has 'actions' field", "actions" in data) | |
| test("actions is list", isinstance(data.get("actions"), list)) | |
| test("actions not empty", len(data.get("actions", [])) > 0) | |
| if data.get("actions"): | |
| action = data["actions"][0] | |
| print(f" Generated {len(data['actions'])} action(s)") | |
| # Validate action structure | |
| test("action has conversation_id", "conversation_id" in action) | |
| test("action has merchant_id", "merchant_id" in action) | |
| test("action has trigger_id", "trigger_id" in action) | |
| test("action has template_name", "template_name" in action) | |
| test("action has body", "body" in action) | |
| test("action has cta", "cta" in action) | |
| test("action has suppression_key", "suppression_key" in action) | |
| test("action has rationale", "rationale" in action) | |
| # Validate action content | |
| test("body is non-empty string", isinstance(action.get("body"), str) and len(action["body"]) > 0) | |
| test("body mentions Dr. Meera", "Dr. Meera" in action.get("body", "")) | |
| test("body mentions 38%", "38%" in action.get("body", "")) | |
| test("body mentions 124", "124" in action.get("body", "")) | |
| test("merchant_id is m_001", action.get("merchant_id") == "m_001") | |
| test("trigger_id is set", action.get("trigger_id") == "trg_test_case_1") | |
| test("template_name is research_digest", "research_digest" in action.get("template_name", "")) | |
| test("cta is valid", action.get("cta") in ["open_ended", "call_action", "question"]) | |
| print(f"\n ✉️ GENERATED MESSAGE:") | |
| print(f" {action['body']}\n") | |
| print(f" CTA: {action['cta']} | Rationale: {action['rationale'][:60]}...") | |
| # ============================================================================ | |
| # TEST 8: Suppression Key Deduplication | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 8: Suppression Key Deduplication (No Duplicate Messages)") | |
| print("="*70) | |
| # Fire same trigger again - should be empty (already sent) | |
| resp = requests.post(f"{BASE_URL}/v1/tick", json=tick_body) | |
| data = resp.json() | |
| test("second tick with same trigger returns empty", len(data.get("actions", [])) == 0) | |
| print(" ✅ Duplicate message suppressed correctly") | |
| # ============================================================================ | |
| # TEST 9: Expired Trigger Handling | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 9: Expired Trigger Handling") | |
| print("="*70) | |
| requests.post(f"{BASE_URL}/v1/teardown", json={}) | |
| requests.post(f"{BASE_URL}/v1/context", json=category_ctx) | |
| requests.post(f"{BASE_URL}/v1/context", json=merchant_ctx) | |
| # Create expired trigger | |
| expired_trigger = { | |
| "scope": "trigger", | |
| "context_id": "trg_expired", | |
| "version": 1, | |
| "payload": { | |
| "id": "trg_expired", | |
| "merchant_id": "m_001", | |
| "kind": "research_digest", | |
| "urgency": 3, | |
| "scope": "merchant", | |
| "suppression_key": "trg_expired_v1", | |
| "source": "internal", | |
| "expires_at": "2026-04-28T00:00:00Z", # 2 days ago | |
| "payload": {"digest_title": "Old News", "key_stat": "Old", "source": "Old"} | |
| } | |
| } | |
| requests.post(f"{BASE_URL}/v1/context", json=expired_trigger) | |
| # Try to fire expired trigger | |
| tick_body_expired = {"now": "2026-04-30T12:15:00Z", "available_triggers": ["trg_expired"]} | |
| resp = requests.post(f"{BASE_URL}/v1/tick", json=tick_body_expired) | |
| data = resp.json() | |
| test("expired trigger returns empty actions", len(data.get("actions", [])) == 0) | |
| print(" ✅ Expired trigger correctly dropped") | |
| # ============================================================================ | |
| # TEST 10: Multiple Triggers (Highest Urgency Priority) | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 10: Multiple Triggers Per Merchant (Highest Priority)") | |
| print("="*70) | |
| requests.post(f"{BASE_URL}/v1/teardown", json={}) | |
| requests.post(f"{BASE_URL}/v1/context", json=category_ctx) | |
| requests.post(f"{BASE_URL}/v1/context", json=merchant_ctx) | |
| # Create two triggers with different urgencies | |
| low_urgency = { | |
| "scope": "trigger", | |
| "context_id": "trg_low_urgency", | |
| "version": 1, | |
| "payload": { | |
| "id": "trg_low_urgency", | |
| "merchant_id": "m_001", | |
| "kind": "research_digest", | |
| "urgency": 1, | |
| "scope": "merchant", | |
| "suppression_key": "low_urgency", | |
| "source": "external", | |
| "expires_at": "2026-05-10T23:59:00Z", | |
| "payload": {"digest_title": "Low Priority", "key_stat": "Low", "source": "External"} | |
| } | |
| } | |
| high_urgency = { | |
| "scope": "trigger", | |
| "context_id": "trg_high_urgency", | |
| "version": 1, | |
| "payload": { | |
| "id": "trg_high_urgency", | |
| "merchant_id": "m_001", | |
| "kind": "research_digest", | |
| "urgency": 4, | |
| "scope": "merchant", | |
| "suppression_key": "high_urgency", | |
| "source": "internal", | |
| "expires_at": "2026-05-10T23:59:00Z", | |
| "payload": {"digest_title": "High Priority", "key_stat": "High", "source": "Internal"} | |
| } | |
| } | |
| requests.post(f"{BASE_URL}/v1/context", json=low_urgency) | |
| requests.post(f"{BASE_URL}/v1/context", json=high_urgency) | |
| # Fire both triggers | |
| tick_body_multi = { | |
| "now": "2026-04-30T12:20:00Z", | |
| "available_triggers": ["trg_low_urgency", "trg_high_urgency"] | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/tick", json=tick_body_multi) | |
| data = resp.json() | |
| # Should pick high urgency (dedupe prevents 2 to same merchant) | |
| test("multi-trigger returns 1 action (dedupe)", len(data.get("actions", [])) == 1) | |
| if data.get("actions"): | |
| action = data["actions"][0] | |
| test("selected high urgency trigger", action.get("trigger_id") == "trg_high_urgency") | |
| print(f" ✅ Selected highest urgency trigger: {action.get('trigger_id')}") | |
| # ============================================================================ | |
| # TEST 11: Context Validation Errors | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 11: Invalid Context (Schema Validation)") | |
| print("="*70) | |
| # Missing required fields | |
| invalid_context = { | |
| "scope": "category", | |
| "context_id": "invalid" | |
| # Missing version and payload | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/context", json=invalid_context) | |
| test("invalid context rejected with 422", resp.status_code == 422) | |
| print(f" ✅ Schema validation working (422 error)") | |
| # ============================================================================ | |
| # TEST 12: Unknown Scope | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST 12: Unknown Scope") | |
| print("="*70) | |
| unknown_scope = { | |
| "scope": "unknown_scope", | |
| "context_id": "test", | |
| "version": 1, | |
| "payload": {} | |
| } | |
| resp = requests.post(f"{BASE_URL}/v1/context", json=unknown_scope) | |
| test("unknown scope rejected", resp.status_code == 200) | |
| data = resp.json() | |
| test("rejected with accepted=false", data.get("accepted") == False) | |
| print(f" ✅ Invalid scope rejected") | |
| # ============================================================================ | |
| # SUMMARY | |
| # ============================================================================ | |
| print("\n" + "="*70) | |
| print("TEST SUMMARY") | |
| print("="*70) | |
| print(f"\n✅ PASSED: {len(TESTS_PASSED)}") | |
| print(f"❌ FAILED: {len(TESTS_FAILED)}") | |
| if TESTS_FAILED: | |
| print(f"\nFailed tests:") | |
| for test_name in TESTS_FAILED: | |
| print(f" - {test_name}") | |
| exit(1) | |
| else: | |
| print("\n🎉 ALL TESTS PASSED! Ready for submission.") | |
| exit(0) | |