mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
327
negot8/test/test_milestone4.py
Normal file
327
negot8/test/test_milestone4.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
test_milestone4.py — Automated success tests for Milestone 4
|
||||
|
||||
Milestone 4 success checklist (from new-milestone.md):
|
||||
✅ run.py is a runnable entry point with run_bots()
|
||||
✅ voice/elevenlabs_tts generates MP3 with cross-platform path
|
||||
✅ build_voice_text returns text for all 8 feature types
|
||||
✅ UPI link generated for expense-type negotiations
|
||||
✅ on_resolution in bot.py wires voice + UPI + analytics
|
||||
✅ analytics stored in DB after negotiation
|
||||
✅ run.py imports cleanly (all modules resolvable)
|
||||
✅ bot.py imports cleanly (voice + UPI imports added)
|
||||
✅ fairness score computed correctly
|
||||
✅ No crashes when ElevenLabs key is missing (fallback to None)
|
||||
|
||||
Run from project root:
|
||||
cd e:\\negot8
|
||||
python test/test_milestone4.py
|
||||
"""
|
||||
import sys, os, asyncio, json, importlib
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BACKEND = os.path.join(ROOT, "backend")
|
||||
BOTS = os.path.join(BACKEND, "telegram-bots")
|
||||
sys.path.insert(0, BACKEND)
|
||||
sys.path.insert(0, BOTS)
|
||||
|
||||
PASS = "✅"
|
||||
FAIL = "❌"
|
||||
results = []
|
||||
|
||||
def record(ok: bool, name: str, detail: str = ""):
|
||||
results.append((PASS if ok else FAIL, name, detail))
|
||||
print(f" {PASS if ok else FAIL} {name}" + (f" [{detail}]" if detail else ""))
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 1. Module import checks
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def test_imports():
|
||||
print("\n── 1. Module imports ──")
|
||||
for mod, label in [
|
||||
("run", "run.py imports cleanly"),
|
||||
("bot", "bot.py imports cleanly"),
|
||||
("voice.elevenlabs_tts", "elevenlabs_tts imports cleanly"),
|
||||
]:
|
||||
try:
|
||||
importlib.import_module(mod)
|
||||
record(True, label)
|
||||
except Exception as e:
|
||||
record(False, label, str(e)[:120])
|
||||
|
||||
# Verify new symbols exist in bot.py
|
||||
try:
|
||||
import bot
|
||||
has_voice = hasattr(bot, "generate_voice_summary") or \
|
||||
"generate_voice_summary" in getattr(bot, "__dict__", {}) or \
|
||||
True # imported into local scope via 'from ... import'
|
||||
from bot import _upi_tool, VOICE_ID_AGENT_A, VOICE_ID_AGENT_B
|
||||
record(True, "bot.py has _upi_tool, VOICE_ID constants")
|
||||
except ImportError as e:
|
||||
record(False, "bot.py has _upi_tool, VOICE_ID constants", str(e))
|
||||
|
||||
# Verify run.py has run_bots
|
||||
try:
|
||||
from run import run_bots
|
||||
record(True, "run.py exposes run_bots()")
|
||||
except ImportError as e:
|
||||
record(False, "run.py exposes run_bots()", str(e))
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 2. run.py entry point structure
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def test_run_py_structure():
|
||||
print("\n── 2. run.py structure ──")
|
||||
import inspect, run
|
||||
|
||||
# run_bots is a coroutine function
|
||||
record(asyncio.iscoroutinefunction(run.run_bots),
|
||||
"run_bots() is an async function")
|
||||
|
||||
# Source contains if __name__ == "__main__"
|
||||
try:
|
||||
src = open(os.path.join(BACKEND, "run.py")).read()
|
||||
record('if __name__ == "__main__"' in src,
|
||||
'run.py has if __name__ == "__main__" block')
|
||||
record("asyncio.run(run_bots())" in src,
|
||||
"run.py calls asyncio.run(run_bots())")
|
||||
except Exception as e:
|
||||
record(False, "run.py source check", str(e))
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 3. ElevenLabs TTS — cross-platform path + templates
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def test_voice_module():
|
||||
print("\n── 3. Voice module ──")
|
||||
from voice.elevenlabs_tts import build_voice_text, VOICE_TEMPLATES
|
||||
|
||||
# All 8 feature types have templates
|
||||
expected = {"expenses", "collaborative", "scheduling", "marketplace",
|
||||
"trip", "freelance", "roommate", "conflict"}
|
||||
actual = set(VOICE_TEMPLATES.keys())
|
||||
record(expected.issubset(actual),
|
||||
"All 8 feature types have voice templates",
|
||||
f"missing: {expected - actual}" if not expected.issubset(actual) else "all present")
|
||||
|
||||
# build_voice_text returns safe string when keys missing
|
||||
text = build_voice_text("scheduling", {"rounds": 3, "date": "Monday", "time": "10am", "location": "Blue Tokai"})
|
||||
record(isinstance(text, str) and len(text) > 5,
|
||||
"build_voice_text returns non-empty string for scheduling",
|
||||
text[:60])
|
||||
|
||||
text2 = build_voice_text("unknown_feature", {"rounds": 2, "summary": "done"})
|
||||
record(isinstance(text2, str) and "negotiation" in text2.lower(),
|
||||
"build_voice_text has safe fallback for unknown feature types",
|
||||
text2[:60])
|
||||
|
||||
# Check no hardcoded /tmp/ path in tts module
|
||||
src = open(os.path.join(BACKEND, "voice", "elevenlabs_tts.py")).read()
|
||||
record("/tmp/" not in src,
|
||||
"elevenlabs_tts.py uses cross-platform path (no hardcoded /tmp/)")
|
||||
record("tempfile.gettempdir()" in src,
|
||||
"elevenlabs_tts.py uses tempfile.gettempdir()")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 4. ElevenLabs TTS — graceful failure without API key
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async def test_voice_graceful_failure():
|
||||
print("\n── 4. Voice graceful failure (no real API call) ──")
|
||||
import unittest.mock as mock
|
||||
from voice.elevenlabs_tts import generate_voice_summary
|
||||
|
||||
# Patch httpx to simulate API error (401)
|
||||
class FakeResp:
|
||||
status_code = 401
|
||||
text = "Unauthorized"
|
||||
|
||||
class FakeClient:
|
||||
async def __aenter__(self): return self
|
||||
async def __aexit__(self, *a): pass
|
||||
async def post(self, *a, **kw): return FakeResp()
|
||||
|
||||
with mock.patch("httpx.AsyncClient", return_value=FakeClient()):
|
||||
result = await generate_voice_summary("test text", "neg_test_001")
|
||||
record(result is None,
|
||||
"generate_voice_summary returns None on API failure (no crash)")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 5. UPI tool works
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async def test_upi_tool():
|
||||
print("\n── 5. UPI tool ──")
|
||||
from tools.upi_generator import UPIGeneratorTool
|
||||
tool = UPIGeneratorTool()
|
||||
result = await tool.execute(
|
||||
payee_upi="alice@upi",
|
||||
payee_name="Alice",
|
||||
amount=500.0,
|
||||
note="negoT8: expenses settlement"
|
||||
)
|
||||
record("upi_link" in result and result["upi_link"].startswith("upi://"),
|
||||
"UPI link generated with upi:// scheme",
|
||||
result.get("upi_link", "")[:60])
|
||||
record("alice@upi" in result["upi_link"],
|
||||
"UPI link contains payee UPI ID")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 6. Analytics DB storage
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async def test_analytics_storage():
|
||||
print("\n── 6. Analytics DB storage ──")
|
||||
import database as db
|
||||
await db.init_db()
|
||||
|
||||
NEG_ID = "m4test001"
|
||||
import aiosqlite
|
||||
from config import DATABASE_PATH
|
||||
async with aiosqlite.connect(DATABASE_PATH) as conn:
|
||||
await conn.execute(
|
||||
"INSERT OR REPLACE INTO negotiations (id, feature_type, initiator_id) VALUES (?, ?, ?)",
|
||||
(NEG_ID, "expenses", 55555)
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
timeline = [{"round": 1, "score_a": 80, "score_b": 60},
|
||||
{"round": 2, "score_a": 85, "score_b": 75}]
|
||||
fairness = 100 - abs(85 - 75) # = 90
|
||||
|
||||
await db.store_analytics({
|
||||
"negotiation_id": NEG_ID,
|
||||
"satisfaction_timeline": json.dumps(timeline),
|
||||
"concession_log": json.dumps([{"round": 1, "by": "A", "gave_up": "morning slot"}]),
|
||||
"fairness_score": fairness,
|
||||
"total_concessions_a": 1,
|
||||
"total_concessions_b": 0,
|
||||
})
|
||||
|
||||
row = await db.get_analytics(NEG_ID)
|
||||
record(row is not None, "Analytics row stored in negotiation_analytics table")
|
||||
if row:
|
||||
row = dict(row)
|
||||
record(abs(row["fairness_score"] - 90) < 0.01,
|
||||
f"Fairness score stored correctly",
|
||||
str(row["fairness_score"]))
|
||||
record(row["total_concessions_a"] == 1,
|
||||
"total_concessions_a stored correctly",
|
||||
str(row["total_concessions_a"]))
|
||||
|
||||
# Cleanup
|
||||
async with aiosqlite.connect(DATABASE_PATH) as conn:
|
||||
await conn.execute("DELETE FROM negotiation_analytics WHERE negotiation_id = ?", (NEG_ID,))
|
||||
await conn.execute("DELETE FROM negotiations WHERE id = ?", (NEG_ID,))
|
||||
await conn.commit()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 7. bot.py on_resolution wiring — check source for voice+analytics
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def test_resolution_wiring():
|
||||
print("\n── 7. on_resolution wiring in bot.py ──")
|
||||
src = open(os.path.join(BOTS, "bot.py")).read()
|
||||
|
||||
record("generate_voice_summary" in src,
|
||||
"bot.py calls generate_voice_summary in on_resolution")
|
||||
record("build_voice_text" in src,
|
||||
"bot.py calls build_voice_text in on_resolution")
|
||||
record("store_analytics" in src,
|
||||
"bot.py calls db.store_analytics in on_resolution")
|
||||
record("upi_link" in src,
|
||||
"bot.py generates UPI link in on_resolution")
|
||||
record("send_voice" in src,
|
||||
"bot.py sends voice note via send_voice in on_resolution")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 8. Decline-before-get bug fixed
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def test_decline_bug_fixed():
|
||||
print("\n── 8. Decline-before-get bug fix ──")
|
||||
src = open(os.path.join(BOTS, "bot.py")).read()
|
||||
|
||||
# After the fix, data = get(...) should come BEFORE pop(...)
|
||||
# within the decline branch
|
||||
decline_block_start = src.find('if action == "decline":')
|
||||
if decline_block_start == -1:
|
||||
record(False, "decline branch found in bot.py")
|
||||
return
|
||||
record(True, "decline branch found in bot.py")
|
||||
|
||||
# Get the slice of text for the decline handler (up to return)
|
||||
decline_slice = src[decline_block_start:decline_block_start + 600]
|
||||
pos_get = decline_slice.find("pending_coordinations.get(neg_id")
|
||||
pos_pop = decline_slice.find("pending_coordinations.pop(neg_id")
|
||||
record(pos_get < pos_pop and pos_get != -1 and pos_pop != -1,
|
||||
"initiator_id is fetched BEFORE pop() in decline branch",
|
||||
f"get@{pos_get} pop@{pos_pop}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Main runner
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
async def run_all():
|
||||
test_imports()
|
||||
test_run_py_structure()
|
||||
test_voice_module()
|
||||
await test_voice_graceful_failure()
|
||||
await test_upi_tool()
|
||||
await test_analytics_storage()
|
||||
test_resolution_wiring()
|
||||
test_decline_bug_fixed()
|
||||
|
||||
passed = sum(1 for s, _, _ in results if s == PASS)
|
||||
total = len(results)
|
||||
print("\n" + "=" * 60)
|
||||
print(f" MILESTONE 4 TESTS: {passed}/{total} passed")
|
||||
print("=" * 60)
|
||||
|
||||
if passed == total:
|
||||
print("""
|
||||
🏆 ALL TESTS PASSED — Milestone 4 is ready!
|
||||
|
||||
══ HOW TO RUN (Live Telegram test) ══════════════════════════
|
||||
cd e:\\negot8\\backend
|
||||
python run.py
|
||||
|
||||
══ FULL FLOW TEST (two phones) ══════════════════════════════
|
||||
Phone A: /start
|
||||
/personality → pick Aggressive (😤)
|
||||
/coordinate @PhoneB_username
|
||||
"Split our hotel bill, I paid ₹8000, john@upi, want 60-40 split"
|
||||
|
||||
Phone B: /start
|
||||
/personality → pick Empathetic (💚)
|
||||
/pending → tap Accept
|
||||
"I think 50-50 is fair, my upi is jane@upi"
|
||||
|
||||
▶ Watch round-by-round updates appear on BOTH phones
|
||||
▶ Final resolution arrives with 📊 satisfaction scores
|
||||
▶ 🎙 Voice note sent to each user (different voices!)
|
||||
▶ 💳 UPI Pay button appended to expense resolutions
|
||||
▶ Check DB: sqlite3 negot8.db "SELECT * FROM negotiation_analytics;"
|
||||
""")
|
||||
else:
|
||||
failed = [(n, d) for s, n, d in results if s == FAIL]
|
||||
print(f"\n⚠️ {total - passed} test(s) failed:")
|
||||
for name, detail in failed:
|
||||
print(f" • {name}")
|
||||
if detail:
|
||||
print(f" {detail}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_all())
|
||||
Reference in New Issue
Block a user