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