import json import importlib import types import unittest from pathlib import Path from unittest.mock import patch import sys ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) # Keep the handler unit test lightweight. The app imports optional runtime # clients at module import time, but this test never calls external services. dotenv_module = types.ModuleType("dotenv") dotenv_module.load_dotenv = lambda *args, **kwargs: None sys.modules.setdefault("dotenv", dotenv_module) openai_module = types.ModuleType("openai") openai_module.OpenAI = object openai_module.APIConnectionError = type("APIConnectionError", (Exception,), {}) openai_module.APIStatusError = type("APIStatusError", (Exception,), {}) openai_module.APITimeoutError = type("APITimeoutError", (Exception,), {}) sys.modules.setdefault("openai", openai_module) pymongo_module = types.ModuleType("pymongo") pymongo_module.MongoClient = object pymongo_errors_module = types.ModuleType("pymongo.errors") pymongo_collection_module = types.ModuleType("pymongo.collection") pymongo_collection_module.Collection = object pymongo_database_module = types.ModuleType("pymongo.database") pymongo_database_module.Database = object pymongo_errors_module.PyMongoError = type("PyMongoError", (Exception,), {}) pymongo_errors_module.ServerSelectionTimeoutError = type( "ServerSelectionTimeoutError", (Exception,), {} ) sys.modules.setdefault("pymongo", pymongo_module) sys.modules.setdefault("pymongo.collection", pymongo_collection_module) sys.modules.setdefault("pymongo.database", pymongo_database_module) sys.modules.setdefault("pymongo.errors", pymongo_errors_module) class FakeServer: def __init__(self): self.routes = [] self.api_routes = {} def _route(self, method, path=None, **kwargs): def decorator(func): self.routes.append((method, path, kwargs, func)) return func return decorator def get(self, path, **kwargs): return self._route("GET", path, **kwargs) def post(self, path, **kwargs): return self._route("POST", path, **kwargs) def api(self, name): def decorator(func): self.api_routes[name] = func return func return decorator def mount(self, *args, **kwargs): return None fastapi_module = types.ModuleType("fastapi") fastapi_module.Body = lambda *args, **kwargs: None fastapi_responses_module = types.ModuleType("fastapi.responses") fastapi_responses_module.HTMLResponse = str fastapi_staticfiles_module = types.ModuleType("fastapi.staticfiles") fastapi_staticfiles_module.StaticFiles = lambda *args, **kwargs: object() gradio_module = types.ModuleType("gradio") gradio_module.Server = FakeServer sys.modules.setdefault("fastapi", fastapi_module) sys.modules.setdefault("fastapi.responses", fastapi_responses_module) sys.modules.setdefault("fastapi.staticfiles", fastapi_staticfiles_module) sys.modules.setdefault("gradio", gradio_module) from core.api_handlers import handle_deck_critique DECK_TEXT = """ EventRadar AI Problem: students miss hackathons because events are scattered across WhatsApp, LinkedIn, Luma, and club pages. Solution: an AI event discovery assistant ranks events by skills, goals, location, and deadline urgency. Users: college students and student founders. Traction: tested with 80 students; 62 missed at least one relevant event last month. Ask: mentor feedback and pilot introductions. """ class DeckCritiqueTests(unittest.TestCase): def test_deck_critique_returns_local_fallback_instead_of_placeholder(self): with patch( "core.api_handlers.model_router.generate_deck_critique_response", return_value={ "ok": False, "model_mode": "premium_nvidia", "provider": "nvidia", "content": "", "error": "model unavailable", }, ): result = handle_deck_critique({"deck_text": DECK_TEXT}) self.assertTrue(result["ok"]) self.assertNotEqual(result.get("status"), "not_implemented") self.assertEqual(result["provider"], "local") self.assertFalse(result["model_ok"]) self.assertTrue(result["summary"]) self.assertGreaterEqual(len(result["strengths"]), 1) self.assertGreaterEqual(len(result["risks"]), 1) self.assertGreaterEqual(len(result["recommended_fixes"]), 1) self.assertTrue(result["next_pitch_question"].endswith("?")) def test_deck_critique_requires_text_or_startup_context(self): result = handle_deck_critique({}) self.assertFalse(result["ok"]) self.assertIn("deck_text", result["error"]) def test_deck_critique_uses_model_json_when_available(self): model_payload = { "summary": "The deck has a focused student-founder wedge but needs sharper proof.", "strengths": ["Clear student pain", "Specific initial traction"], "risks": ["Business model is still vague"], "recommended_fixes": ["Add one slide on who pays and why this is urgent"], "next_pitch_question": "Who pays for this, and what evidence shows they will pay?", } with patch( "core.api_handlers.model_router.generate_deck_critique_response", return_value={ "ok": True, "model_mode": "premium_nvidia", "provider": "nvidia", "content": json.dumps(model_payload), "error": None, }, create=True, ): result = handle_deck_critique( {"deck_text": DECK_TEXT, "model_mode": "premium_nvidia"} ) self.assertTrue(result["ok"]) self.assertTrue(result["model_ok"]) self.assertEqual(result["provider"], "nvidia") self.assertEqual(result["summary"], model_payload["summary"]) self.assertEqual(result["recommended_fixes"], model_payload["recommended_fixes"]) def test_deck_critique_is_registered_as_gradio_api(self): pitchfight_app = importlib.import_module("app") self.assertIn("deck_critique", pitchfight_app.app.api_routes) with patch( "app.handle_deck_critique", return_value={"ok": True, "summary": "registered"}, ) as handler: result = pitchfight_app.app.api_routes["deck_critique"]({"deck_text": DECK_TEXT}) handler.assert_called_once_with({"deck_text": DECK_TEXT}) self.assertEqual(result["summary"], "registered") if __name__ == "__main__": unittest.main()