import json import os import sqlite3 import threading import unittest from http.client import HTTPConnection from tempfile import TemporaryDirectory from unittest.mock import Mock from app.ai_opportunity import normalize_assessment from app.ai_research import configure_db as configure_ai_research_db from app.main import create_server, hash_password class OpportunityNormalizationTests(unittest.TestCase): def test_normalizes_scores_enums_and_known_evidence_only(self): provider = Mock(return_value={ "opportunity_score": 81, "confidence_score": 0.82, "recommendation": "contact", "priority": "high", "reasons": ["Two independent public listings corroborate the business."], "missing_evidence": [], "website_assessment": {"status": "healthy", "broken": False, "outdated": False, "mobile_issue": False, "https_issue": False, "performance_issue": False}, "domain_assessment": {"status": "registered"}, "contactability": {"public_business_contact_found": True, "contact_type": "general_business"}, "recommended_services": ["seo"], "evidence_references": [2, 1, 2], "human_review_required": False, }) result = normalize_assessment(provider(), {1, 2}) self.assertEqual(result["opportunity_score"], 81) self.assertEqual(result["confidence_score"], 82) self.assertEqual(result["evidence_references"], [1, 2]) self.assertFalse(result["human_review_required"]) self.assertEqual(result["recommendation"], "contact") self.assertEqual(result["reasons"], ["Two independent public listings corroborate the business."]) self.assertEqual(result["missing_evidence"], []) self.assertEqual(result["website_assessment"]["status"], "healthy") self.assertEqual(result["domain_assessment"]["status"], "registered") self.assertEqual(result["contactability"]["contact_type"], "general_business") self.assertEqual(result["recommended_services"], ["seo"]) def test_contact_recommendation_is_an_internal_no_send_recommendation(self): payload = {"recommendation": "contact", "evidence_references": [1, 2], "confidence_score": 90} self.assertEqual(normalize_assessment(payload, {1, 2})["recommendation"], "contact") def test_unknown_values_default_and_weak_evidence_requires_human_review(self): result = normalize_assessment({ "opportunity_score": "not-a-number", "confidence_score": 20, "recommendation": "email_them_now", "priority": "urgent", "evidence_references": [], "human_review_required": False, }, set()) self.assertEqual(result["opportunity_score"], 0) self.assertEqual(result["confidence_score"], 20) self.assertEqual(result["recommendation"], "insufficient_evidence") self.assertEqual(result["priority"], "low") self.assertTrue(result["human_review_required"]) def test_unknown_evidence_reference_is_rejected(self): with self.assertRaisesRegex(ValueError, "unknown_evidence_reference"): normalize_assessment({"evidence_references": [99]}, {1}) def test_suppression_overrides_provider_recommendation(self): result = normalize_assessment({ "opportunity_score": 99, "confidence_score": 99, "recommendation": "contact", "priority": "high", "evidence_references": [1, 2], "human_review_required": False, }, {1, 2}, suppressed=True) self.assertEqual(result["recommendation"], "do_not_contact") self.assertTrue(result["human_review_required"]) class OpportunityAssessmentApiTests(unittest.TestCase): def setUp(self): self.tmp = TemporaryDirectory() self.old = {key: os.environ.get(key) for key in ("AI_PROVIDER", "BOOTSTRAP_ADMIN_EMAIL", "BOOTSTRAP_ADMIN_PASSWORD")} os.environ.update({"AI_PROVIDER": "local", "BOOTSTRAP_ADMIN_EMAIL": "opportunity-owner@example.test", "BOOTSTRAP_ADMIN_PASSWORD": "password"}) self.server = create_server("127.0.0.1", 0, self.tmp.name + "/opportunity.db") self.thread = threading.Thread(target=self.server.serve_forever, daemon=True); self.thread.start() self.conn = HTTPConnection("127.0.0.1", self.server.server_port, timeout=3); self.cookie = None self.request("POST", "/api/v1/auth/login", {"email": "opportunity-owner@example.test", "password": "password"}) def tearDown(self): self.server.shutdown(); self.server.server_close(); self.thread.join(timeout=2) configure_ai_research_db("") self.tmp.cleanup() for key, value in self.old.items(): if value is None: os.environ.pop(key, None) else: os.environ[key] = value def request(self, method, path, payload=None): headers = {"Content-Type": "application/json"} if self.cookie: headers["Cookie"] = self.cookie self.conn.request(method, path, json.dumps(payload).encode() if payload is not None else None, headers) response = self.conn.getresponse(); cookie = response.getheader("Set-Cookie") if cookie: self.cookie = cookie.split(";", 1)[0] return response.status, json.loads(response.read() or b"{}") def selected_business_at_threshold(self): status, business = self.request("POST", "/api/v1/businesses", {"name": "Selected Co"}) self.assertEqual(status, 201) db = sqlite3.connect(self.tmp.name + "/opportunity.db") db.execute("UPDATE businesses SET score=70 WHERE id=?", (business["id"],)); db.commit(); db.close() return business["id"] def test_manual_selected_business_after_threshold_returns_grounded_review_only_assessment(self): bid = self.selected_business_at_threshold() self.request("POST", f"/api/v1/businesses/{bid}/evidence", {"kind": "source", "url": "https://source.test", "claim": "Needs a modern website"}) status, result = self.request("POST", f"/api/v1/businesses/{bid}/ai/opportunity-assessment", {}) self.assertEqual(status, 201) self.assertEqual(result["business_id"], bid) self.assertEqual(result["assessment"]["evidence_references"], [1]) self.assertIn("opportunity_score", result["assessment"]) self.assertIn("confidence_score", result["assessment"]) self.assertTrue(result["assessment"]["human_review_required"]) self.assertFalse(result["network_send"]) self.assertFalse(result["automatic_outreach"]) status, runs = self.request("GET", "/api/v1/ai-runs") self.assertEqual(status, 200) self.assertEqual(runs["items"][0]["output"]["assessment"], result["assessment"]) self.assertEqual(len(runs["items"][0]["input_evidence_hashes"]), 1) def test_manual_assessment_requires_deterministic_threshold(self): status, business = self.request("POST", "/api/v1/businesses", {"name": "Below threshold"}) self.assertEqual(status, 201) status, result = self.request("POST", f"/api/v1/businesses/{business['id']}/ai/opportunity-assessment", {}) self.assertEqual(status, 409) self.assertEqual(result["error"], "deterministic_threshold_not_met") def test_unconfigured_provider_fails_closed_without_assessment_output(self): bid = self.selected_business_at_threshold() with unittest.mock.patch("app.main.provider_status", return_value={"status": "not_configured", "provider": ""}): status, result = self.request("POST", f"/api/v1/businesses/{bid}/ai/opportunity-assessment", {}) self.assertEqual(status, 409) self.assertEqual(result["error"], "ai_provider_not_configured") self.assertFalse(result["network_send"]) self.assertNotIn("assessment", result) def test_suppressed_selected_business_assessment_is_do_not_contact(self): bid = self.selected_business_at_threshold() self.request("POST", f"/api/v1/businesses/{bid}/evidence", {"kind": "source", "url": "https://source.test", "claim": "Evidence"}) self.request("POST", "/api/v1/suppressions", {"kind": "domain", "value": "selected.test"}) db = sqlite3.connect(self.tmp.name + "/opportunity.db") db.execute("UPDATE businesses SET website_domain='selected.test' WHERE id=?", (bid,)); db.commit(); db.close() status, result = self.request("POST", f"/api/v1/businesses/{bid}/ai/opportunity-assessment", {}) self.assertEqual(status, 201) self.assertEqual(result["assessment"]["recommendation"], "do_not_contact") self.assertTrue(result["assessment"]["human_review_required"]) def test_manual_assessment_is_tenant_scoped(self): bid = self.selected_business_at_threshold() password_hash, password_salt = hash_password("other-password") db = sqlite3.connect(self.tmp.name + "/opportunity.db") db.execute("INSERT INTO organizations(id,name) VALUES(?,?)", ("other-tenant", "Other")) db.execute("INSERT INTO users(organization_id,email,password_hash,password_salt,role) VALUES(?,?,?,?,?)", ("other-tenant", "other-opportunity@example.test", password_hash, password_salt, "owner")) db.commit(); db.close() self.cookie = None self.assertEqual(self.request("POST", "/api/v1/auth/login", {"email": "other-opportunity@example.test", "password": "other-password"})[0], 200) self.assertEqual(self.request("POST", f"/api/v1/businesses/{bid}/ai/opportunity-assessment", {})[0], 404) if __name__ == "__main__": unittest.main()