Files
MarketingTool/apps/api/tests/test_ai_opportunity.py
T

171 lines
9.5 KiB
Python
Raw Normal View History

2026-09-04 20:52:37 +02:00
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()