#!/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)