This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View 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
View 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!")

View 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())

View 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())

View 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())

View 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())

View 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())

View 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())

View 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
View 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())