vera-bot / comprehensive_tests.py
Dov-tek
Restructure: rule-based decision engine + template slot-filling architecture
a1757c5
Raw
History Blame Contribute Delete
14.2 kB
#!/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)