mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
init
This commit is contained in:
112
negot8/test/bot_runner_test.py
Normal file
112
negot8/test/bot_runner_test.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Bot Runner Test - Run the full negoT8 bot with all features
|
||||
Run this from anywhere: python test/bot_runner_test.py
|
||||
|
||||
Runs Bot A AND Bot B simultaneously so the full /pending cross-bot flow works:
|
||||
Phone A → /coordinate @userB → enters preferences
|
||||
Phone B → /pending → sees request, taps Accept
|
||||
Phone B → enters preferences → agents negotiate live
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# Add backend to path so we can import modules
|
||||
backend_path = os.path.join(os.path.dirname(__file__), '..', 'backend')
|
||||
sys.path.insert(0, backend_path)
|
||||
|
||||
# Now import the bot creator and config
|
||||
from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B
|
||||
import database as db
|
||||
|
||||
# Import bot after path is set
|
||||
telegram_bots_path = os.path.join(backend_path, 'telegram-bots')
|
||||
sys.path.insert(0, telegram_bots_path)
|
||||
|
||||
from bot import create_bot
|
||||
from telegram import Update
|
||||
|
||||
|
||||
async def setup_database():
|
||||
"""Initialize database tables if needed."""
|
||||
print("🗄️ Initializing database...")
|
||||
await db.init_db()
|
||||
print("✅ Database ready!")
|
||||
|
||||
|
||||
async def run_bots_async():
|
||||
"""Start both Bot A and Bot B concurrently so pending_coordinations is shared."""
|
||||
bots = []
|
||||
|
||||
if TELEGRAM_BOT_TOKEN_A:
|
||||
bot_a = create_bot(TELEGRAM_BOT_TOKEN_A)
|
||||
bots.append(("A", bot_a))
|
||||
print(f"✅ Bot A configured: {TELEGRAM_BOT_TOKEN_A[:20]}...")
|
||||
else:
|
||||
print("⚠️ TELEGRAM_BOT_TOKEN_A not set — skipping Bot A")
|
||||
|
||||
if TELEGRAM_BOT_TOKEN_B:
|
||||
bot_b = create_bot(TELEGRAM_BOT_TOKEN_B)
|
||||
bots.append(("B", bot_b))
|
||||
print(f"✅ Bot B configured: {TELEGRAM_BOT_TOKEN_B[:20]}...")
|
||||
else:
|
||||
print("⚠️ TELEGRAM_BOT_TOKEN_B not set — skipping Bot B (only 1 bot)")
|
||||
|
||||
if not bots:
|
||||
print("❌ No bot tokens found. Check your .env file.")
|
||||
return
|
||||
|
||||
print(f"\n🚀 Launching {len(bots)} bot(s) in parallel...")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Initialize all bots and run them concurrently
|
||||
tasks = []
|
||||
for name, app in bots:
|
||||
await app.initialize()
|
||||
await app.start()
|
||||
print(f"▶️ Bot {name} is polling...")
|
||||
tasks.append(app.updater.start_polling(allowed_updates=Update.ALL_TYPES))
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run Bot A (and optionally Bot B) with full feature set."""
|
||||
print("=" * 60)
|
||||
print("🤖 Starting negoT8 Bots (Full Feature Set)")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("📋 Features enabled:")
|
||||
print(" • /start — Welcome message")
|
||||
print(" • /personality — Set agent negotiation style (5 types)")
|
||||
print(" • /coordinate @u — Start a coordination request (User A)")
|
||||
print(" • /pending — View & accept incoming requests (User B)")
|
||||
print(" • /help — View all commands")
|
||||
print()
|
||||
|
||||
if not TELEGRAM_BOT_TOKEN_A and not TELEGRAM_BOT_TOKEN_B:
|
||||
print("❌ ERROR: No bot tokens found in environment!")
|
||||
print(" Make sure your .env file has TELEGRAM_BOT_TOKEN_A (and optionally _B) set.")
|
||||
return
|
||||
|
||||
# Setup database
|
||||
asyncio.run(setup_database())
|
||||
|
||||
print("🚀 Bots are now running... Press Ctrl+C to stop")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
try:
|
||||
asyncio.run(run_bots_async())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Bots stopped by user")
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ Bot crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
47
negot8/test/test_apis.py
Normal file
47
negot8/test/test_apis.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# test_apis.py (run this standalone — tests Gemini, Tavily, ElevenLabs)
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# ─── Test 1: Gemini ───
|
||||
print("Testing Gemini...")
|
||||
import google.generativeai as genai
|
||||
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
||||
model = genai.GenerativeModel(
|
||||
model_name="gemini-3-flash-preview",
|
||||
generation_config=genai.GenerationConfig(
|
||||
response_mime_type="application/json",
|
||||
temperature=0.7,
|
||||
)
|
||||
)
|
||||
response = model.generate_content(
|
||||
'Return JSON: {"status": "ok", "message": "Gemini works"}'
|
||||
)
|
||||
print(f" Gemini: {response.text}")
|
||||
|
||||
# ─── Test 2: Tavily ───
|
||||
print("\nTesting Tavily...")
|
||||
from tavily import TavilyClient
|
||||
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
||||
result = tavily.search("best Thai restaurants in Bandra Mumbai", include_answer=True, max_results=2)
|
||||
print(f" Tavily answer: {result.get('answer', 'No answer')[:150]}")
|
||||
print(f" Results count: {len(result.get('results', []))}")
|
||||
|
||||
# ─── Test 3: ElevenLabs ───
|
||||
print("\nTesting ElevenLabs...")
|
||||
import httpx
|
||||
resp = httpx.post(
|
||||
"https://api.elevenlabs.io/v1/text-to-speech/ZthjuvLPty3kTMaNKVKb",
|
||||
headers={"xi-api-key": os.getenv("ELEVENLABS_API_KEY"), "Content-Type": "application/json"},
|
||||
json={"text": "Hello from negoT8!", "model_id": "eleven_flash_v2_5",
|
||||
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}},
|
||||
timeout=15.0
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
with open("test_voice.mp3", "wb") as f:
|
||||
f.write(resp.content)
|
||||
print(f" ElevenLabs: ✅ Saved test_voice.mp3 ({len(resp.content)} bytes)")
|
||||
else:
|
||||
print(f" ElevenLabs: ❌ {resp.status_code} — {resp.text[:200]}")
|
||||
|
||||
print("\n✅ All API tests complete!")
|
||||
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())
|
||||
412
negot8/test/test_milestone5_all.py
Normal file
412
negot8/test/test_milestone5_all.py
Normal file
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
Milestone 5 — Full Feature Test Suite
|
||||
Run from backend/ directory:
|
||||
cd backend && python ../test/test_milestone5_all.py
|
||||
|
||||
Tests all 8 feature modules locally (no Telegram) against live Gemini + Tavily APIs.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/backend")
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
PASS = "✅"
|
||||
FAIL = "❌"
|
||||
SKIP = "⏭"
|
||||
|
||||
results = {}
|
||||
|
||||
|
||||
# ─── Test helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def check(name: str, condition: bool, detail: str = ""):
|
||||
icon = PASS if condition else FAIL
|
||||
results[name] = condition
|
||||
print(f" {icon} {name}" + (f": {detail}" if detail else ""))
|
||||
|
||||
|
||||
# ─── 0. Database init ─────────────────────────────────────────────────────────
|
||||
|
||||
async def test_database():
|
||||
print("\n── Test 0: Database Init ──")
|
||||
from database import init_db
|
||||
try:
|
||||
await init_db()
|
||||
check("init_db runs", True)
|
||||
except Exception as e:
|
||||
check("init_db runs", False, str(e))
|
||||
|
||||
from database import (
|
||||
create_user, get_user, create_negotiation,
|
||||
add_participant, get_rounds, store_analytics, get_analytics
|
||||
)
|
||||
await create_user(99999, "testuser", "Test User")
|
||||
user = await get_user(99999)
|
||||
check("create_user + get_user", user is not None)
|
||||
|
||||
neg_id = await create_negotiation("expenses", 99999)
|
||||
check("create_negotiation", len(neg_id) > 0, f"id={neg_id}")
|
||||
|
||||
await add_participant(neg_id, 99999, {"goal": "test"}, "balanced")
|
||||
check("add_participant", True)
|
||||
|
||||
rows = await get_rounds(neg_id)
|
||||
check("get_rounds returns list", isinstance(rows, list))
|
||||
|
||||
await store_analytics({
|
||||
"negotiation_id": neg_id,
|
||||
"satisfaction_timeline": "[]",
|
||||
"concession_log": "[]",
|
||||
"fairness_score": 80.0,
|
||||
"total_concessions_a": 2,
|
||||
"total_concessions_b": 1,
|
||||
})
|
||||
analytics = await get_analytics(neg_id)
|
||||
check("store + get analytics", analytics is not None and analytics.get("fairness_score") == 80.0)
|
||||
|
||||
|
||||
# ─── 1. Feature context fetching ─────────────────────────────────────────────
|
||||
|
||||
async def test_feature_contexts():
|
||||
print("\n── Test 1: Feature Context Fetching ──")
|
||||
|
||||
from features.base_feature import get_feature
|
||||
|
||||
PREFS = {
|
||||
"scheduling": (
|
||||
{"raw_details": {"available_windows": ["Monday 10am-12pm", "Wednesday 3-5pm"], "location": "Bandra"}, "goal": "Schedule a coffee meeting", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
{"raw_details": {"available_windows": ["Monday 11am-1pm", "Friday 2-4pm"]}, "goal": "Find a time to meet", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"expenses": (
|
||||
{"raw_details": {"expenses": [{"name": "Hotel", "amount": 12000}, {"name": "Fuel", "amount": 3000}], "upi_id": "rahul@paytm"}, "goal": "Split trip expenses", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
{"raw_details": {"expenses": [{"name": "Dinner", "amount": 2000}]}, "goal": "Fair expense split", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"collaborative": (
|
||||
{"raw_details": {"cuisine": "Italian", "location": "Bandra", "budget": 800}, "goal": "Pick dinner spot", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
{"raw_details": {"cuisine": "Chinese", "location": "Bandra", "budget": 600}, "goal": "Restaurant for tonight", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"marketplace": (
|
||||
{"raw_details": {"item": "PS5 with 2 controllers", "asking_price": 35000, "minimum_price": 30000, "role": "seller", "upi_id": "seller@upi"}, "goal": "Sell PS5", "constraints": [], "preferences": [], "tone": "firm"},
|
||||
{"raw_details": {"item": "PS5", "budget": 28000, "role": "buyer"}, "goal": "Buy PS5", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"freelance": (
|
||||
{"raw_details": {"skill": "React developer", "rate": 1500, "hours": 40, "upfront_minimum": "50"}, "goal": "Freelance React project", "constraints": [{"value": "minimum rate ₹1500/hr", "hard": True}], "preferences": [], "tone": "professional"},
|
||||
{"raw_details": {"budget": 40000, "project_type": "web app", "required_features": ["auth", "dashboard", "API"], "role": "client"}, "goal": "Build web app in budget", "constraints": [{"value": "budget max ₹40000", "hard": True}], "preferences": [], "tone": "professional"},
|
||||
),
|
||||
"roommate": (
|
||||
{"raw_details": {"decision_type": "wifi plan", "city": "Mumbai", "budget": 600}, "goal": "Pick WiFi plan", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
{"raw_details": {"decision_type": "wifi plan", "city": "Mumbai", "budget": 700}, "goal": "Fast internet within budget", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"trip": (
|
||||
{"raw_details": {"available_dates": ["March 15-17", "March 22-24"], "budget": 5000, "destination_preference": "beach", "origin": "Mumbai"}, "goal": "Weekend beach trip", "constraints": [], "preferences": [], "tone": "excited"},
|
||||
{"raw_details": {"available_dates": ["March 15-17", "April 5-7"], "budget": 4000, "destination_preference": "hills", "origin": "Mumbai"}, "goal": "Weekend getaway", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
"conflict": (
|
||||
{"raw_details": {"conflict_type": "parking spot", "position": "I need the spot Mon-Wed", "relationship_importance": "high"}, "goal": "Resolve parking dispute", "constraints": [], "preferences": [], "tone": "firm"},
|
||||
{"raw_details": {"conflict_type": "parking spot", "position": "I need the spot Tue-Thu", "relationship_importance": "high"}, "goal": "Fair parking arrangement", "constraints": [], "preferences": [], "tone": "friendly"},
|
||||
),
|
||||
}
|
||||
|
||||
for feature_type, (prefs_a, prefs_b) in PREFS.items():
|
||||
try:
|
||||
feat = get_feature(feature_type)
|
||||
ctx = await feat.get_context(prefs_a, prefs_b)
|
||||
check(f"{feature_type}.get_context", len(ctx) > 50, f"{len(ctx)} chars")
|
||||
except Exception as e:
|
||||
check(f"{feature_type}.get_context", False, str(e))
|
||||
|
||||
|
||||
# ─── 2. Full negotiation for each feature (live Gemini) ──────────────────────
|
||||
|
||||
async def test_negotiation_feature(feature_type: str, prefs_a: dict, prefs_b: dict, check_fn=None):
|
||||
"""Run a full negotiation for one feature and verify the result."""
|
||||
from agents.negotiation import run_negotiation
|
||||
from features.base_feature import get_feature
|
||||
import database as db
|
||||
|
||||
feat = get_feature(feature_type)
|
||||
try:
|
||||
feature_context = await feat.get_context(prefs_a, prefs_b)
|
||||
except Exception:
|
||||
feature_context = ""
|
||||
|
||||
neg_id = await db.create_negotiation(feature_type, 99901)
|
||||
await db.add_participant(neg_id, 99901, prefs_a)
|
||||
await db.add_participant(neg_id, 99902, prefs_b)
|
||||
|
||||
resolution = await run_negotiation(
|
||||
negotiation_id=neg_id,
|
||||
preferences_a=prefs_a,
|
||||
preferences_b=prefs_b,
|
||||
user_a_id=99901,
|
||||
user_b_id=99902,
|
||||
feature_type=feature_type,
|
||||
personality_a="balanced",
|
||||
personality_b="balanced",
|
||||
feature_context=feature_context,
|
||||
)
|
||||
|
||||
check(f"{feature_type} negotiation completes", resolution is not None)
|
||||
check(f"{feature_type} has status", resolution.get("status") in ("resolved", "escalated"),
|
||||
resolution.get("status"))
|
||||
check(f"{feature_type} has rounds_taken", isinstance(resolution.get("rounds_taken"), int),
|
||||
str(resolution.get("rounds_taken")))
|
||||
check(f"{feature_type} has final_proposal", isinstance(resolution.get("final_proposal"), dict))
|
||||
|
||||
# Feature-specific checks
|
||||
if check_fn:
|
||||
check_fn(resolution, prefs_a, prefs_b)
|
||||
|
||||
# Verify format_resolution produces non-empty string
|
||||
try:
|
||||
formatted = feat.format_resolution(resolution, prefs_a, prefs_b)
|
||||
check(f"{feature_type} format_resolution non-empty", len(formatted) > 30, f"{len(formatted)} chars")
|
||||
print(f"\n 📄 Formatted resolution preview:\n {formatted[:200].replace(chr(10), chr(10)+' ')}\n")
|
||||
except Exception as e:
|
||||
check(f"{feature_type} format_resolution", False, str(e))
|
||||
|
||||
return resolution
|
||||
|
||||
|
||||
async def test_all_negotiations():
|
||||
print("\n── Test 2: Full Negotiations (live Gemini + Tavily) ──")
|
||||
print(" (This may take 2-4 minutes due to API rate limits)\n")
|
||||
|
||||
from database import init_db
|
||||
await init_db()
|
||||
|
||||
# ── Feature 1: Scheduling ──
|
||||
print(" [1/8] Scheduling...")
|
||||
await test_negotiation_feature(
|
||||
"scheduling",
|
||||
{"goal": "Schedule a coffee meeting", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "scheduling",
|
||||
"raw_details": {"available_windows": ["Monday 10am-12pm", "Wednesday 3-5pm"], "location": "Bandra", "duration": "1 hour"}},
|
||||
{"goal": "Coffee meeting next week", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "scheduling",
|
||||
"raw_details": {"available_windows": ["Monday 11am-1pm", "Wednesday 4-6pm"]}},
|
||||
)
|
||||
|
||||
# ── Feature 2: Expenses ──
|
||||
print(" [2/8] Expenses...")
|
||||
|
||||
def check_expense_math(resolution, prefs_a, prefs_b):
|
||||
# Verify the resolution doesn't hallucinate wrong math
|
||||
final = resolution.get("final_proposal", {})
|
||||
details = final.get("details", {})
|
||||
# At minimum, the proposal should exist
|
||||
check("expenses has details", bool(details), str(details)[:80])
|
||||
|
||||
await test_negotiation_feature(
|
||||
"expenses",
|
||||
{"goal": "Split Goa trip expenses fairly", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "expenses",
|
||||
"raw_details": {"expenses": [{"name": "Hotel", "amount": 12000}, {"name": "Fuel", "amount": 3000}], "upi_id": "rahul@paytm"}},
|
||||
{"goal": "Fair expense split", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "expenses",
|
||||
"raw_details": {"expenses": [{"name": "Dinner", "amount": 2000}]}},
|
||||
check_fn=check_expense_math,
|
||||
)
|
||||
|
||||
# ── Feature 3: Collaborative ──
|
||||
print(" [3/8] Collaborative (uses Tavily)...")
|
||||
|
||||
def check_collaborative(resolution, prefs_a, prefs_b):
|
||||
final = resolution.get("final_proposal", {})
|
||||
summary = final.get("summary", "")
|
||||
check("collaborative has venue recommendation", bool(summary), summary[:60])
|
||||
|
||||
await test_negotiation_feature(
|
||||
"collaborative",
|
||||
{"goal": "Pick a restaurant for dinner", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "collaborative",
|
||||
"raw_details": {"cuisine": "Italian", "location": "Bandra Mumbai", "budget": 800}},
|
||||
{"goal": "Dinner somewhere nice", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "collaborative",
|
||||
"raw_details": {"cuisine": "Chinese", "location": "Bandra Mumbai", "budget": 600}},
|
||||
check_fn=check_collaborative,
|
||||
)
|
||||
|
||||
# ── Feature 4: Marketplace ──
|
||||
print(" [4/8] Marketplace (uses Tavily)...")
|
||||
|
||||
def check_marketplace(resolution, prefs_a, prefs_b):
|
||||
final = resolution.get("final_proposal", {})
|
||||
details = final.get("details", {})
|
||||
price = details.get("agreed_price") or details.get("price") or details.get("final_price") or ""
|
||||
reasoning = resolution.get("summary", "")
|
||||
check("marketplace has price", bool(price) or bool(reasoning), f"price={price}")
|
||||
|
||||
await test_negotiation_feature(
|
||||
"marketplace",
|
||||
{"goal": "Sell my PS5", "constraints": [{"value": "minimum ₹30000", "hard": True}],
|
||||
"preferences": [], "tone": "firm", "feature_type": "marketplace",
|
||||
"raw_details": {"item": "PS5 with 2 controllers", "asking_price": 35000, "minimum_price": 30000, "role": "seller", "upi_id": "seller@upi"}},
|
||||
{"goal": "Buy a PS5 in budget", "constraints": [{"value": "budget max ₹28000", "hard": True}],
|
||||
"preferences": [], "tone": "friendly", "feature_type": "marketplace",
|
||||
"raw_details": {"item": "PS5", "budget": 28000, "role": "buyer", "max_budget": 28000}},
|
||||
check_fn=check_marketplace,
|
||||
)
|
||||
|
||||
# ── Feature 5: Freelance ──
|
||||
print(" [5/8] Freelance (uses Tavily + Calculator)...")
|
||||
|
||||
def check_freelance(resolution, prefs_a, prefs_b):
|
||||
final = resolution.get("final_proposal", {})
|
||||
details = final.get("details", {})
|
||||
budget = details.get("budget") or details.get("agreed_budget") or details.get("price") or ""
|
||||
check("freelance has budget in proposal", bool(budget) or bool(details), f"budget={budget}")
|
||||
|
||||
await test_negotiation_feature(
|
||||
"freelance",
|
||||
{"goal": "Get paid fairly for React project", "constraints": [{"value": "min rate ₹1500/hr", "hard": True}],
|
||||
"preferences": [], "tone": "professional", "feature_type": "freelance",
|
||||
"raw_details": {"skill": "React developer", "rate": 1500, "hours": 40, "upfront_minimum": "50"}},
|
||||
{"goal": "Build web app within ₹40k budget", "constraints": [{"value": "budget max ₹40000", "hard": True}],
|
||||
"preferences": [], "tone": "professional", "feature_type": "freelance",
|
||||
"raw_details": {"budget": 40000, "project_type": "web app", "required_features": ["auth", "dashboard", "API"], "role": "client"}},
|
||||
check_fn=check_freelance,
|
||||
)
|
||||
|
||||
# ── Feature 6: Roommate ──
|
||||
print(" [6/8] Roommate (uses Tavily for real plans)...")
|
||||
|
||||
def check_roommate(resolution, prefs_a, prefs_b):
|
||||
final = resolution.get("final_proposal", {})
|
||||
summary = final.get("summary", "")
|
||||
check("roommate has decision", bool(summary), summary[:60])
|
||||
|
||||
await test_negotiation_feature(
|
||||
"roommate",
|
||||
{"goal": "Pick a shared WiFi plan", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "roommate",
|
||||
"raw_details": {"decision_type": "wifi plan", "city": "Mumbai", "budget": 600}},
|
||||
{"goal": "Fast internet within budget", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "roommate",
|
||||
"raw_details": {"decision_type": "wifi plan", "city": "Mumbai", "budget": 700}},
|
||||
check_fn=check_roommate,
|
||||
)
|
||||
|
||||
# ── Feature 7: Conflict ──
|
||||
print(" [7/8] Conflict Resolution...")
|
||||
|
||||
await test_negotiation_feature(
|
||||
"conflict",
|
||||
{"goal": "Resolve parking spot dispute", "constraints": [], "preferences": [],
|
||||
"tone": "firm", "feature_type": "conflict",
|
||||
"raw_details": {"conflict_type": "parking spot", "position": "I need Mon-Wed", "relationship_importance": "high"}},
|
||||
{"goal": "Fair parking arrangement", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "conflict",
|
||||
"raw_details": {"conflict_type": "parking spot", "position": "I need Tue-Thu", "relationship_importance": "high"}},
|
||||
)
|
||||
|
||||
# ── Feature 8: Trip (Group Negotiation) ──
|
||||
print(" [8/8] Trip Planning (group negotiation with 3 preferences)...")
|
||||
from features.trip import run_group_negotiation
|
||||
from database import init_db as _init
|
||||
|
||||
all_prefs = [
|
||||
{"goal": "Beach weekend trip", "constraints": [], "preferences": [],
|
||||
"tone": "excited", "feature_type": "trip",
|
||||
"raw_details": {"available_dates": ["March 15-17", "March 22-24"], "budget": 5000, "destination_preference": "beach", "origin": "Mumbai"}},
|
||||
{"goal": "Weekend trip with friends", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "trip",
|
||||
"raw_details": {"available_dates": ["March 15-17", "April 5-7"], "budget": 4000, "destination_preference": "hills", "origin": "Mumbai"}},
|
||||
{"goal": "Budget getaway", "constraints": [], "preferences": [],
|
||||
"tone": "friendly", "feature_type": "trip",
|
||||
"raw_details": {"available_dates": ["March 15-17", "March 29-31"], "budget": 3500, "destination_preference": "any", "origin": "Mumbai"}},
|
||||
]
|
||||
from database import create_negotiation, add_participant
|
||||
trip_neg_id = await create_negotiation("trip", 99901)
|
||||
for i, p in enumerate(all_prefs):
|
||||
await add_participant(trip_neg_id, 99901 + i, p)
|
||||
|
||||
trip_resolution = {"status": None}
|
||||
async def on_trip_round(data):
|
||||
print(f" Round {data['round_number']}: {data['action']} | avg_sat={data['satisfaction_score']:.0f}")
|
||||
|
||||
async def on_trip_resolve(data):
|
||||
trip_resolution["status"] = data.get("status")
|
||||
trip_resolution["summary"] = data.get("summary", "")
|
||||
trip_resolution["data"] = data
|
||||
|
||||
await run_group_negotiation(
|
||||
negotiation_id=trip_neg_id,
|
||||
all_preferences=all_prefs,
|
||||
all_user_ids=[99901, 99902, 99903],
|
||||
feature_type="trip",
|
||||
personalities=["balanced", "empathetic", "balanced"],
|
||||
on_round_update=on_trip_round,
|
||||
on_resolution=on_trip_resolve,
|
||||
)
|
||||
check("trip group negotiation completes", trip_resolution["status"] in ("resolved", "escalated"),
|
||||
trip_resolution.get("status"))
|
||||
check("trip has summary", bool(trip_resolution.get("summary")))
|
||||
|
||||
from features.trip import TripFeature
|
||||
if trip_resolution.get("data"):
|
||||
try:
|
||||
formatted = TripFeature().format_resolution(trip_resolution["data"], all_prefs[0], all_prefs[1])
|
||||
check("trip format_resolution", len(formatted) > 30, f"{len(formatted)} chars")
|
||||
except Exception as e:
|
||||
check("trip format_resolution", False, str(e))
|
||||
|
||||
|
||||
# ─── 3. Dispatcher test ───────────────────────────────────────────────────────
|
||||
|
||||
async def test_dispatcher():
|
||||
print("\n── Test 3: Feature Dispatcher ──")
|
||||
from features.base_feature import get_feature
|
||||
from features.scheduling import SchedulingFeature
|
||||
from features.expenses import ExpensesFeature
|
||||
from features.collaborative import CollaborativeFeature
|
||||
from features.marketplace import MarketplaceFeature
|
||||
from features.freelance import FreelanceFeature
|
||||
from features.roommate import RoommateFeature
|
||||
from features.trip import TripFeature
|
||||
from features.conflict import ConflictFeature
|
||||
from features.generic import GenericFeature
|
||||
|
||||
for feature_type, expected_cls in [
|
||||
("scheduling", SchedulingFeature), ("expenses", ExpensesFeature),
|
||||
("collaborative", CollaborativeFeature), ("marketplace", MarketplaceFeature),
|
||||
("freelance", FreelanceFeature), ("roommate", RoommateFeature),
|
||||
("trip", TripFeature), ("conflict", ConflictFeature),
|
||||
("generic", GenericFeature), ("unknown_xyz", GenericFeature),
|
||||
]:
|
||||
feat = get_feature(feature_type)
|
||||
check(f"get_feature('{feature_type}')", isinstance(feat, expected_cls))
|
||||
|
||||
|
||||
# ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print(" negoT8 — Milestone 5 Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
await test_database()
|
||||
await test_dispatcher()
|
||||
await test_feature_contexts()
|
||||
# Full negotiations use live Gemini + Tavily — skip if you want a fast run
|
||||
await test_all_negotiations()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
total = len(results)
|
||||
passed = sum(1 for v in results.values() if v)
|
||||
failed = total - passed
|
||||
print(f" RESULTS: {passed}/{total} passed" + (f" | {failed} FAILED" if failed else " ✅ All passed!"))
|
||||
print("=" * 60)
|
||||
|
||||
if failed:
|
||||
print("\nFailed tests:")
|
||||
for name, status in results.items():
|
||||
if not status:
|
||||
print(f" ❌ {name}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
59
negot8/test/test_negotiation.py
Normal file
59
negot8/test/test_negotiation.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# test_negotiation.py
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
backend_path = os.path.join(os.path.dirname(__file__), '..', 'backend')
|
||||
sys.path.insert(0, backend_path)
|
||||
async def on_round(data):
|
||||
print(f"\n🔄 Round {data['round_number']}: {data['action']}")
|
||||
print(f" Satisfaction A: {data.get('satisfaction_a', '?')}% B: {data.get('satisfaction_b', '?')}%")
|
||||
print(f" Reasoning: {data['reasoning']}")
|
||||
|
||||
async def on_resolve(data):
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🏁 RESULT: {data['status']} in {data['rounds_taken']} rounds")
|
||||
print(f" Summary: {data['summary']}")
|
||||
print(f" Timeline: {json.dumps(data.get('satisfaction_timeline', []))}")
|
||||
|
||||
async def test():
|
||||
from agents.negotiation import run_negotiation
|
||||
import database as db
|
||||
|
||||
await db.init_db()
|
||||
|
||||
# Test with DIFFERENT personalities: aggressive vs empathetic
|
||||
prefs_a = {
|
||||
"feature_type": "expenses",
|
||||
"goal": "Split Goa trip expenses",
|
||||
"constraints": [{"type": "budget", "value": None, "description": "Fair split", "hard": True}],
|
||||
"preferences": [
|
||||
{"type": "split", "value": "60-40 fuel", "priority": "high", "description": "I drove the whole way"},
|
||||
{"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI preferred"}
|
||||
],
|
||||
"raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000, "upi_id": "rahul@paytm"}
|
||||
}
|
||||
prefs_b = {
|
||||
"feature_type": "expenses",
|
||||
"goal": "Split Goa trip expenses fairly",
|
||||
"constraints": [{"type": "fairness", "value": "equal contribution acknowledged", "hard": False}],
|
||||
"preferences": [
|
||||
{"type": "split", "value": "50-50 fuel", "priority": "high", "description": "I navigated and planned"},
|
||||
{"type": "payment", "value": "UPI", "priority": "medium", "description": "UPI fine"}
|
||||
],
|
||||
"raw_details": {"hotel": 12000, "fuel": 3000, "dinner": 2000}
|
||||
}
|
||||
|
||||
neg_id = await db.create_negotiation("expenses", 111)
|
||||
await db.add_participant(neg_id, 111, prefs_a, personality_used="aggressive")
|
||||
await db.add_participant(neg_id, 222, prefs_b, personality_used="empathetic")
|
||||
|
||||
print("🧪 Testing: AGGRESSIVE (A) vs EMPATHETIC (B) on expense splitting\n")
|
||||
result = await run_negotiation(
|
||||
negotiation_id=neg_id, preferences_a=prefs_a, preferences_b=prefs_b,
|
||||
user_a_id=111, user_b_id=222, feature_type="expenses",
|
||||
personality_a="aggressive", personality_b="empathetic",
|
||||
on_round_update=on_round, on_resolution=on_resolve
|
||||
)
|
||||
|
||||
asyncio.run(test())
|
||||
281
negot8/test/test_pdf_generator.py
Normal file
281
negot8/test/test_pdf_generator.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
test_pdf_generator.py — Tests for negoT8 Deal Agreement PDF generation
|
||||
|
||||
Run from the project root:
|
||||
cd /path/to/negot8
|
||||
python test/test_pdf_generator.py
|
||||
|
||||
What this tests:
|
||||
1. Freelance deal PDF — rich details (budget, scope, timeline, payment)
|
||||
2. Marketplace (buy/sell) PDF — item, price, delivery
|
||||
3. Minimal data — all optional fields absent; should still produce a valid PDF
|
||||
4. Blockchain proof attached — real-looking proof dict
|
||||
5. Mock blockchain proof — mock=True path
|
||||
6. File is actually written to /tmp and is non-empty
|
||||
7. Temp-file cleanup helper works
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ── Make sure backend/ is on the path ────────────────────────────────────────
|
||||
BACKEND = os.path.join(os.path.dirname(__file__), "..", "backend")
|
||||
sys.path.insert(0, os.path.abspath(BACKEND))
|
||||
|
||||
from tools.pdf_generator import generate_deal_pdf
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Shared fixtures
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
USER_A = {"id": 111111111, "name": "Alice Sharma", "username": "alice_s"}
|
||||
USER_B = {"id": 222222222, "name": "Bob Chatterjee", "username": "bobchat"}
|
||||
|
||||
REAL_PROOF = {
|
||||
"success": True,
|
||||
"mock": False,
|
||||
"tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
"block_number": 87654321,
|
||||
"agreement_hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"explorer_url": "https://amoy.polygonscan.com/tx/0xabcdef1234567890",
|
||||
"gas_used": 42000,
|
||||
}
|
||||
|
||||
MOCK_PROOF = {
|
||||
"success": True,
|
||||
"mock": True,
|
||||
"tx_hash": "0xMOCKTX1234567890",
|
||||
"block_number": 0,
|
||||
"agreement_hash": "0xMOCKHASH1234567890",
|
||||
"explorer_url": "",
|
||||
"gas_used": 0,
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Test helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _check_pdf(path: str, label: str):
|
||||
"""Assert the file exists, is non-empty, and starts with the PDF magic bytes."""
|
||||
assert os.path.exists(path), f"[{label}] PDF file not found at {path}"
|
||||
size = os.path.getsize(path)
|
||||
assert size > 500, f"[{label}] PDF suspiciously small: {size} bytes"
|
||||
with open(path, "rb") as f:
|
||||
magic = f.read(4)
|
||||
assert magic == b"%PDF", f"[{label}] File does not start with PDF magic bytes: {magic}"
|
||||
print(f" ✅ [{label}] PDF OK — {size:,} bytes → {path}")
|
||||
|
||||
|
||||
def _cleanup(path: str):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Individual tests
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def test_freelance_full():
|
||||
"""Freelance deal with full details + real blockchain proof."""
|
||||
final_proposal = {
|
||||
"summary": "Alice will build the dashboard in 3 weeks for Rs. 45,000 with 50% upfront.",
|
||||
"details": {
|
||||
"budget": "45000",
|
||||
"timeline": "3 weeks",
|
||||
"scope": ["Admin dashboard", "REST API integration", "Mobile responsive UI"],
|
||||
"payment_schedule": "50% upfront (Rs. 22,500) + 50% on delivery",
|
||||
"upfront": "22500",
|
||||
"ip_ownership": "Full transfer to client on final payment",
|
||||
},
|
||||
}
|
||||
preferences_a = {
|
||||
"goal": "Build a dashboard",
|
||||
"raw_details": {
|
||||
"role": "freelancer", "skill": "React + FastAPI",
|
||||
"rate": "1500", "hours": "30", "upfront_minimum": "50",
|
||||
},
|
||||
}
|
||||
preferences_b = {
|
||||
"goal": "Hire a developer",
|
||||
"raw_details": {
|
||||
"role": "client", "budget": "45000",
|
||||
"required_features": ["Admin dashboard", "API", "Mobile UI"],
|
||||
},
|
||||
}
|
||||
|
||||
path = await generate_deal_pdf(
|
||||
negotiation_id = "freelance_test_001",
|
||||
feature_type = "freelance",
|
||||
final_proposal = final_proposal,
|
||||
user_a = USER_A,
|
||||
user_b = USER_B,
|
||||
rounds_taken = 4,
|
||||
sat_a = 85.0,
|
||||
sat_b = 78.0,
|
||||
preferences_a = preferences_a,
|
||||
preferences_b = preferences_b,
|
||||
blockchain_proof = REAL_PROOF,
|
||||
)
|
||||
_check_pdf(path, "freelance_full")
|
||||
_cleanup(path)
|
||||
|
||||
|
||||
async def test_marketplace_full():
|
||||
"""Buy/sell deal with full details + mock blockchain proof."""
|
||||
final_proposal = {
|
||||
"summary": "iPhone 14 sold for Rs. 52,000. Pickup at Andheri station on Saturday.",
|
||||
"details": {
|
||||
"agreed_price": "52000",
|
||||
"delivery": "Pickup — Andheri West Metro station, Saturday 4 PM",
|
||||
"condition": "Used — 6 months old, no scratches",
|
||||
"market_price": "55000",
|
||||
},
|
||||
}
|
||||
preferences_a = {
|
||||
"goal": "Sell iPhone 14",
|
||||
"raw_details": {
|
||||
"role": "seller", "item": "iPhone 14 128GB Black",
|
||||
"asking_price": "55000", "minimum_price": "50000",
|
||||
},
|
||||
}
|
||||
preferences_b = {
|
||||
"goal": "Buy iPhone 14",
|
||||
"raw_details": {
|
||||
"role": "buyer", "item": "iPhone 14",
|
||||
"max_budget": "54000", "offer_price": "49000",
|
||||
},
|
||||
}
|
||||
|
||||
path = await generate_deal_pdf(
|
||||
negotiation_id = "marketplace_test_001",
|
||||
feature_type = "marketplace",
|
||||
final_proposal = final_proposal,
|
||||
user_a = USER_A,
|
||||
user_b = USER_B,
|
||||
rounds_taken = 3,
|
||||
sat_a = 72.0,
|
||||
sat_b = 88.0,
|
||||
preferences_a = preferences_a,
|
||||
preferences_b = preferences_b,
|
||||
blockchain_proof = MOCK_PROOF,
|
||||
)
|
||||
_check_pdf(path, "marketplace_full")
|
||||
_cleanup(path)
|
||||
|
||||
|
||||
async def test_minimal_data():
|
||||
"""Both feature_type is unknown and all optional fields are empty — should not crash."""
|
||||
path = await generate_deal_pdf(
|
||||
negotiation_id = "minimal_test_001",
|
||||
feature_type = "generic",
|
||||
final_proposal = {"summary": "Parties agreed to share expenses equally."},
|
||||
user_a = {"id": 1, "name": "", "username": "userA"},
|
||||
user_b = {"id": 2, "name": "", "username": "userB"},
|
||||
rounds_taken = 1,
|
||||
sat_a = 60.0,
|
||||
sat_b = 60.0,
|
||||
blockchain_proof = None,
|
||||
)
|
||||
_check_pdf(path, "minimal_data")
|
||||
_cleanup(path)
|
||||
|
||||
|
||||
async def test_no_blockchain_proof():
|
||||
"""Freelance deal where blockchain proof hasn't been registered yet."""
|
||||
final_proposal = {
|
||||
"summary": "React Native app, Rs. 80,000, 6 weeks.",
|
||||
"details": {
|
||||
"budget": "80000",
|
||||
"timeline": "6 weeks",
|
||||
"scope": ["React Native app", "Backend API"],
|
||||
},
|
||||
}
|
||||
path = await generate_deal_pdf(
|
||||
negotiation_id = "noproof_test_001",
|
||||
feature_type = "freelance",
|
||||
final_proposal = final_proposal,
|
||||
user_a = USER_A,
|
||||
user_b = USER_B,
|
||||
rounds_taken = 5,
|
||||
sat_a = 90.0,
|
||||
sat_b = 70.0,
|
||||
blockchain_proof = None,
|
||||
)
|
||||
_check_pdf(path, "no_blockchain_proof")
|
||||
_cleanup(path)
|
||||
|
||||
|
||||
async def test_unicode_safe():
|
||||
"""
|
||||
Ensure the PDF builder doesn't crash on characters outside Latin-1
|
||||
(Rs. symbol ₹, em-dashes, etc.).
|
||||
"""
|
||||
final_proposal = {
|
||||
"summary": "₹45,000 deal — React dashboard — agreed ✓",
|
||||
"details": {
|
||||
"budget": "₹45,000",
|
||||
"timeline": "3 weeks – confirmed",
|
||||
"scope": ["Dashboard • Admin panel", "API • REST"],
|
||||
},
|
||||
}
|
||||
path = await generate_deal_pdf(
|
||||
negotiation_id = "unicode_test_001",
|
||||
feature_type = "freelance",
|
||||
final_proposal = final_proposal,
|
||||
user_a = {"id": 1, "name": "Anirbán Bāsak", "username": "anirban"},
|
||||
user_b = {"id": 2, "name": "Rāhul Gupta", "username": "rahul"},
|
||||
rounds_taken = 2,
|
||||
sat_a = 88.0,
|
||||
sat_b = 82.0,
|
||||
blockchain_proof = MOCK_PROOF,
|
||||
)
|
||||
_check_pdf(path, "unicode_safe")
|
||||
_cleanup(path)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Runner
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
TESTS = [
|
||||
("Freelance full details + real blockchain proof", test_freelance_full),
|
||||
("Marketplace (buy/sell) + mock proof", test_marketplace_full),
|
||||
("Minimal / sparse data — no crash", test_minimal_data),
|
||||
("No blockchain proof yet", test_no_blockchain_proof),
|
||||
("Unicode / special chars — Latin-1 safety", test_unicode_safe),
|
||||
]
|
||||
|
||||
|
||||
async def main():
|
||||
print("\n🧪 negoT8 — PDF Generator Tests")
|
||||
print("=" * 52)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, fn in TESTS:
|
||||
print(f"\n▶ {name}")
|
||||
try:
|
||||
await fn()
|
||||
passed += 1
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
print(f" ❌ FAILED: {exc}")
|
||||
traceback.print_exc()
|
||||
failed += 1
|
||||
|
||||
print("\n" + "=" * 52)
|
||||
print(f"Results: {passed} passed | {failed} failed | {len(TESTS)} total")
|
||||
|
||||
if failed == 0:
|
||||
print("✅ All PDF tests passed!\n")
|
||||
else:
|
||||
print("❌ Some tests failed — see output above.\n")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
269
negot8/test/test_pending_flow.py
Normal file
269
negot8/test/test_pending_flow.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
test_pending_flow.py — Success test for Step 5: /pending counterparty flow
|
||||
|
||||
Tests (no live Telegram connection needed):
|
||||
✅ 1. pending_coordinations dict is populated when User A coordinates
|
||||
✅ 2. /pending lookup by counterparty username returns correct entries
|
||||
✅ 3. Personality is stored in the pending entry
|
||||
✅ 4. Accepting a request marks it as "accepted" and stores neg_id in user_data
|
||||
✅ 5. Declining a request removes it from pending_coordinations
|
||||
✅ 6. receive_counterparty_preferences persists BOTH participants in DB
|
||||
✅ 7. Both participants have personality_used saved in participants table
|
||||
✅ 8. Negotiation is created in DB with correct feature_type
|
||||
✅ 9. Bot module imports cleanly (all handlers registered)
|
||||
✅ 10. send_to_user silently fails when no bots are registered
|
||||
|
||||
Run from project root:
|
||||
cd e:\\negot8
|
||||
python test/test_pending_flow.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
# ─── Path setup ───
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BACKEND = os.path.join(ROOT, "backend")
|
||||
BOTS_DIR = os.path.join(BACKEND, "telegram-bots")
|
||||
sys.path.insert(0, BACKEND)
|
||||
sys.path.insert(0, BOTS_DIR)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
PASS = "✅"
|
||||
FAIL = "❌"
|
||||
results: list[tuple[str, str, str]] = [] # (status, name, detail)
|
||||
|
||||
def record(ok: bool, name: str, detail: str = ""):
|
||||
status = PASS if ok else FAIL
|
||||
results.append((status, name, detail))
|
||||
print(f" {status} {name}" + (f" [{detail}]" if detail else ""))
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Test 1 — Module import & handler registration
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
def test_module_import():
|
||||
print("\n── Test 1: Bot module imports cleanly ──")
|
||||
try:
|
||||
import bot # noqa: F401
|
||||
record(True, "bot.py imports without errors")
|
||||
except Exception as e:
|
||||
record(False, "bot.py import", str(e))
|
||||
return None
|
||||
|
||||
try:
|
||||
from bot import (
|
||||
pending_coordinations, bot_apps, register_bot, send_to_user,
|
||||
pending_command, accept_pending_callback,
|
||||
receive_counterparty_preferences, run_negotiation_with_telegram_updates,
|
||||
create_bot, AWAITING_PREFERENCES, AWAITING_COUNTERPARTY_PREFS,
|
||||
)
|
||||
record(True, "All new symbols exported from bot.py")
|
||||
return True
|
||||
except ImportError as e:
|
||||
record(False, "Symbol import check", str(e))
|
||||
return False
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Test 2 — pending_coordinations dict logic
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
async def test_pending_dict_logic():
|
||||
print("\n── Test 2: pending_coordinations dict logic ──")
|
||||
import bot
|
||||
|
||||
# Reset shared dict for a clean test
|
||||
bot.pending_coordinations.clear()
|
||||
|
||||
NEG_ID = "test1234"
|
||||
PREFS_A = {"feature_type": "scheduling", "goal": "coffee next week",
|
||||
"constraints": [], "preferences": []}
|
||||
PERSONALITY = "aggressive"
|
||||
|
||||
# Simulate User A calling /coordinate and storing in dict (mirrors receive_preferences)
|
||||
bot.pending_coordinations[NEG_ID] = {
|
||||
"negotiation_id": NEG_ID,
|
||||
"counterparty_username": "bob", # lowercase username
|
||||
"initiator_id": 111,
|
||||
"initiator_name": "Alice",
|
||||
"feature_type": "scheduling",
|
||||
"preferences_a": PREFS_A,
|
||||
"personality_a": PERSONALITY,
|
||||
"status": "pending",
|
||||
}
|
||||
|
||||
# /pending lookup: User B (bob) checks for their requests
|
||||
username_b = "bob"
|
||||
matching = {
|
||||
nid: d for nid, d in bot.pending_coordinations.items()
|
||||
if d.get("counterparty_username", "").lower() == username_b
|
||||
and d.get("status") == "pending"
|
||||
}
|
||||
record(len(matching) == 1, "/pending finds the request by counterparty username",
|
||||
f"found {len(matching)} entry(ies)")
|
||||
|
||||
# Personality is stored
|
||||
entry = matching[NEG_ID]
|
||||
record(entry.get("personality_a") == PERSONALITY, "Personality_a stored in pending dict",
|
||||
entry.get("personality_a"))
|
||||
|
||||
# Accept: mark as accepted, simulating accept_pending_callback
|
||||
bot.pending_coordinations[NEG_ID]["status"] = "accepted"
|
||||
still_pending = {
|
||||
nid: d for nid, d in bot.pending_coordinations.items()
|
||||
if d.get("counterparty_username", "").lower() == username_b
|
||||
and d.get("status") == "pending"
|
||||
}
|
||||
record(len(still_pending) == 0,
|
||||
"After accept, /pending no longer shows the same request")
|
||||
|
||||
# Decline: should remove entry
|
||||
bot.pending_coordinations["test9999"] = {
|
||||
"negotiation_id": "test9999",
|
||||
"counterparty_username": "carol",
|
||||
"initiator_id": 222,
|
||||
"initiator_name": "Dave",
|
||||
"feature_type": "expenses",
|
||||
"preferences_a": {},
|
||||
"personality_a": "balanced",
|
||||
"status": "pending",
|
||||
}
|
||||
bot.pending_coordinations.pop("test9999", None)
|
||||
record("test9999" not in bot.pending_coordinations, "Declined request removed from dict")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Test 3 — Database: both participants + personality persisted
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
async def test_db_persistence():
|
||||
print("\n── Test 3: DB persistence of both participants ──")
|
||||
import database as db
|
||||
|
||||
await db.init_db()
|
||||
|
||||
NEG_ID = "pendtest1"
|
||||
USER_A = 10001
|
||||
USER_B = 10002
|
||||
|
||||
PREFS_A = {"feature_type": "scheduling", "goal": "coffee meeting",
|
||||
"constraints": [], "preferences": []}
|
||||
PREFS_B = {"feature_type": "scheduling", "goal": "afternoon slot",
|
||||
"constraints": [], "preferences": []}
|
||||
|
||||
# Ensure users exist
|
||||
await db.create_user(USER_A, "alice_test", "Alice")
|
||||
await db.create_user(USER_B, "bob_test", "Bob")
|
||||
|
||||
# Create negotiation + add both participants (mirrors the full flow)
|
||||
await db.create_negotiation.__wrapped__(NEG_ID, "scheduling", USER_A) \
|
||||
if hasattr(db.create_negotiation, "__wrapped__") else None
|
||||
|
||||
# Use raw DB insert for test isolation
|
||||
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, "scheduling", USER_A)
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
await db.add_participant(NEG_ID, USER_A, PREFS_A, personality_used="aggressive")
|
||||
await db.add_participant(NEG_ID, USER_B, PREFS_B, personality_used="empathetic")
|
||||
|
||||
participants = await db.get_participants(NEG_ID)
|
||||
record(len(participants) == 2, "Both participants stored in DB", f"count={len(participants)}")
|
||||
|
||||
personalities = {dict(p)["user_id"]: dict(p)["personality_used"] for p in participants}
|
||||
record(personalities.get(USER_A) == "aggressive",
|
||||
"User A personality_used='aggressive' persisted", str(personalities.get(USER_A)))
|
||||
record(personalities.get(USER_B) == "empathetic",
|
||||
"User B personality_used='empathetic' persisted", str(personalities.get(USER_B)))
|
||||
|
||||
# Cleanup
|
||||
import aiosqlite
|
||||
from config import DATABASE_PATH
|
||||
async with aiosqlite.connect(DATABASE_PATH) as conn:
|
||||
await conn.execute("DELETE FROM participants WHERE negotiation_id = ?", (NEG_ID,))
|
||||
await conn.execute("DELETE FROM negotiations WHERE id = ?", (NEG_ID,))
|
||||
await conn.commit()
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Test 4 — send_to_user fails gracefully when no bots registered
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
async def test_send_to_user_graceful():
|
||||
print("\n── Test 4: send_to_user graceful failure ──")
|
||||
import bot
|
||||
|
||||
bot.bot_apps.clear() # No bots registered
|
||||
result = await bot.send_to_user(99999, "test message")
|
||||
record(result is False, "send_to_user returns False when no bots are registered")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Test 5 — Conversation state constants are correct
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
def test_conversation_states():
|
||||
print("\n── Test 5: ConversationHandler state constants ──")
|
||||
from bot import AWAITING_PREFERENCES, AWAITING_COUNTERPARTY_PREFS
|
||||
record(AWAITING_PREFERENCES == 1,
|
||||
"AWAITING_PREFERENCES == 1", str(AWAITING_PREFERENCES))
|
||||
record(AWAITING_COUNTERPARTY_PREFS == 2,
|
||||
"AWAITING_COUNTERPARTY_PREFS == 2", str(AWAITING_COUNTERPARTY_PREFS))
|
||||
record(AWAITING_PREFERENCES != AWAITING_COUNTERPARTY_PREFS,
|
||||
"States are distinct (no collision)")
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════
|
||||
# Main runner
|
||||
# ════════════════════════════════════════════════
|
||||
|
||||
async def run_all():
|
||||
ok = test_module_import()
|
||||
if ok is False:
|
||||
print("\n⛔ Aborting — bot.py failed to import. Fix import errors first.")
|
||||
return
|
||||
|
||||
await test_pending_dict_logic()
|
||||
await test_db_persistence()
|
||||
await test_send_to_user_graceful()
|
||||
test_conversation_states()
|
||||
|
||||
# ── Summary ──
|
||||
passed = sum(1 for s, _, _ in results if s == PASS)
|
||||
total = len(results)
|
||||
print("\n" + "=" * 60)
|
||||
print(f" PENDING FLOW TESTS: {passed}/{total} passed")
|
||||
print("=" * 60)
|
||||
|
||||
if passed == total:
|
||||
print("\n🏆 ALL TESTS PASSED — /pending counterparty flow is ready!\n")
|
||||
print("Next steps (live Telegram test):")
|
||||
print(" 1. Run: python test/bot_runner_test.py")
|
||||
print(" 2. Phone A: /coordinate @PhoneB_username")
|
||||
print(" 3. Phone A: describe your preferences")
|
||||
print(" 4. Phone B: /pending → tap Accept")
|
||||
print(" 5. Phone B: describe their preferences")
|
||||
print(" 6. Watch agents negotiate live on both phones! 🤖↔️🤖\n")
|
||||
else:
|
||||
failed = [(name, detail) for s, name, detail in results if s == FAIL]
|
||||
print(f"\n⚠️ {total - passed} test(s) failed:")
|
||||
for name, detail in failed:
|
||||
print(f" • {name}: {detail}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_all())
|
||||
37
negot8/test/test_personal_agent.py
Normal file
37
negot8/test/test_personal_agent.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
|
||||
|
||||
from agents.personal_agent import PersonalAgent
|
||||
from personality.profiles import get_personality_modifier
|
||||
|
||||
async def test():
|
||||
agent = PersonalAgent()
|
||||
|
||||
# Test 1: Scheduling
|
||||
r1 = await agent.extract_preferences("Find time for coffee with Priya next week. I'm free Mon-Wed afternoons.")
|
||||
print("TEST 1 (scheduling):", r1.get("feature_type"), "✅" if r1.get("feature_type") == "scheduling" else "❌")
|
||||
|
||||
# Test 2: Expenses (with UPI mention)
|
||||
r2 = await agent.extract_preferences("Split our Goa trip costs. I paid 12K hotel, 3K fuel. Fuel should be 60-40 since I drove. My UPI is rahul@paytm")
|
||||
print("TEST 2 (expenses):", r2.get("feature_type"), "✅" if r2.get("feature_type") == "expenses" else "❌")
|
||||
print(" UPI extracted:", "rahul@paytm" in json.dumps(r2), "✅" if "rahul@paytm" in json.dumps(r2) else "⚠️ UPI not found")
|
||||
|
||||
# Test 3: Marketplace
|
||||
r3 = await agent.extract_preferences("I want to sell my PS5 to this guy. Asking 35K, minimum 30K, has 2 controllers.")
|
||||
print("TEST 3 (marketplace):", r3.get("feature_type"), "✅" if r3.get("feature_type") == "marketplace" else "❌")
|
||||
|
||||
# Test 4: Generic
|
||||
r4 = await agent.extract_preferences("Figure out with @dave who brings what to the BBQ party Saturday")
|
||||
print("TEST 4 (generic):", r4.get("feature_type"), "✅" if r4.get("feature_type") in ("generic", "collaborative") else "❌")
|
||||
|
||||
# Test 5: Personality profiles load
|
||||
for p in ["aggressive", "people_pleaser", "analytical", "empathetic", "balanced"]:
|
||||
mod = get_personality_modifier(p)
|
||||
print(f"PERSONALITY {p}: {'✅' if len(mod) > 50 else '❌'} ({len(mod)} chars)")
|
||||
|
||||
print("\nFull output Test 2:", json.dumps(r2, indent=2))
|
||||
|
||||
asyncio.run(test())
|
||||
19
negot8/test/test_telegram.py
Normal file
19
negot8/test/test_telegram.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# test_telegram.py (run standalone)
|
||||
import asyncio
|
||||
import sys
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend'))
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
await update.message.reply_text("🤖 negoT8 Bot A is alive!")
|
||||
|
||||
app = Application.builder().token(os.getenv("TELEGRAM_BOT_TOKEN_A")).build()
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
|
||||
print("Bot A running... Press Ctrl+C to stop")
|
||||
app.run_polling()
|
||||
26
negot8/test/test_tools.py
Normal file
26
negot8/test/test_tools.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
backend_path = os.path.join(os.path.dirname(__file__), '..', 'backend')
|
||||
sys.path.insert(0, backend_path)
|
||||
from tools.tavily_search import TavilySearchTool
|
||||
from tools.upi_generator import UPIGeneratorTool
|
||||
from tools.calculator import CalculatorTool
|
||||
|
||||
async def test():
|
||||
# Tavily
|
||||
tavily = TavilySearchTool()
|
||||
r = await tavily.execute("best Thai restaurants Bandra Mumbai")
|
||||
print(f"Tavily: {r['answer'][:100]}... ({len(r['results'])} results) ✅")
|
||||
|
||||
# UPI
|
||||
upi = UPIGeneratorTool()
|
||||
r = await upi.execute("rahul@paytm", "Rahul", 8200, "Goa trip settlement")
|
||||
print(f"UPI: {r['upi_link'][:60]}... ✅")
|
||||
|
||||
# Calculator
|
||||
calc = CalculatorTool()
|
||||
r = await calc.execute("12000 * 0.55")
|
||||
print(f"Calc: 12000 * 0.55 = {r['result']} ✅")
|
||||
|
||||
asyncio.run(test())
|
||||
Reference in New Issue
Block a user