replace default scoring with opportunity model
CI / compose (push) Successful in 13m53s

This commit is contained in:
Marco0300
2026-09-03 23:21:35 +02:00
parent 00bd49a894
commit cb31f2dd04
4 changed files with 137 additions and 36 deletions
+45 -5
View File
@@ -7,16 +7,54 @@ from http.client import HTTPConnection
from tempfile import TemporaryDirectory
from app.main import create_server
from app.scoring import DEFAULT_RULES, evaluate_score
from app.scoring import DEFAULT_RULES, SCORE_VERSION, evaluate_score, signals_for_business
class ScoringEngineTests(unittest.TestCase):
def test_opportunity_defaults_are_the_exact_versioned_transparent_model(self):
expected = {
"no_detected_website": 30, "no_official_domain": 25,
"no_functioning_web_service": 25, "broken_website": 25,
"parked_default_placeholder": 20, "public_free_mail": 15,
"human_reviewed_outdated": 15, "no_working_https": 10,
"severe_performance": 10, "active_social": 10,
"valid_public_business_phone": 5, "multiple_corroborating_sources": 5,
"possibly_closed": -30, "healthy_modern_website": -30,
"stale_or_uncertain": -15,
}
self.assertEqual(SCORE_VERSION, "opportunity-v1")
self.assertEqual({r["code"]: r["points"] for r in DEFAULT_RULES}, expected)
self.assertTrue(all(r["version"] == 1 and r["enabled"] == 1 for r in DEFAULT_RULES))
def test_each_opportunity_signal_applies_only_when_present_and_explains_rule_metadata(self):
for rule in DEFAULT_RULES:
signals = {"state": {"suppressed": False, "merge_status": "active"}, "opportunity": {rule["code"]: True}}
result = evaluate_score(signals, [rule])
self.assertEqual(result["score"], max(0, rule["points"]), rule["code"])
explanation = result["explanations"][0]
self.assertEqual({explanation[k] for k in ("code", "name", "points", "version")}, {rule["code"], rule["name"], rule["points"], 1})
def test_signal_extraction_maps_website_contacts_and_provenance_to_opportunity_signals(self):
signals = signals_for_business({"name": "Acme", "website": "http://acme.test", "website_domain": "acme.test", "phone": "+27123456789"},
{"classification": "parked", "social_signal": True, "performance": "severe", "human_reviewed_outdated": True},
[{"public_business": True, "classification": "free_mail", "suppressed": False, "do_not_contact": False}],
{"status": "unknown", "official": False}, False, sources=["directory", "registry"])
self.assertTrue(signals["opportunity"]["parked_default_placeholder"])
self.assertTrue(signals["opportunity"]["public_free_mail"])
self.assertTrue(signals["opportunity"]["active_social"])
self.assertTrue(signals["opportunity"]["multiple_corroborating_sources"])
def test_score_cap_and_negative_signals_are_deterministic(self):
signals = {"opportunity": {r["code"]: True for r in DEFAULT_RULES}, "state": {"suppressed": False, "merge_status": "active"}}
first = evaluate_score(signals, DEFAULT_RULES)
self.assertEqual(first["score"], 100)
self.assertEqual(first, evaluate_score(signals, list(reversed(DEFAULT_RULES))))
def test_defaults_are_deterministic_and_emit_explanations_and_band(self):
signals = {"business": {"name": "Acme", "email": "a@acme.test", "website_domain": "acme.test"}, "website": {"classification": "healthy"}, "state": {"suppressed": False}}
first = evaluate_score(signals, DEFAULT_RULES)
self.assertEqual(first, evaluate_score(signals, DEFAULT_RULES))
self.assertEqual(0 <= first["score"] <= 100, True)
self.assertEqual(first["priority_band"], "medium")
self.assertEqual(first["priority_band"], "low")
self.assertTrue(all("code" in item and "reason" in item for item in first["explanations"]))
def test_disabled_and_versioned_rules_change_score_without_nondeterminism(self):
@@ -26,12 +64,14 @@ class ScoringEngineTests(unittest.TestCase):
self.assertEqual(enabled["score"], 30)
self.assertEqual(disabled["score"], 0)
def test_suppression_is_ineligible_and_stale_uncertain_signals_do_not_penalize(self):
signals = {"business": {"name": "Acme"}, "website": {"classification": "unknown", "stale": True}, "domain": {"status": "error"}, "state": {"suppressed": True}}
def test_suppression_is_ineligible_and_stale_uncertain_signal_is_transparent(self):
signals = {"business": {"name": "Acme"}, "website": {"classification": "unknown", "stale": True}, "domain": {"status": "error"}, "opportunity": {"stale_or_uncertain": True}, "state": {"suppressed": True}}
result = evaluate_score(signals, DEFAULT_RULES)
self.assertFalse(result["eligible"])
self.assertEqual(result["priority_band"], "ineligible")
self.assertNotIn("negative", json.dumps(result["explanations"]).lower())
stale = next(item for item in result["explanations"] if item["code"] == "stale_or_uncertain")
self.assertTrue(stale["applied"])
self.assertEqual(stale["points"], -15)
class ScoringApiTests(unittest.TestCase):
+2
View File
@@ -93,6 +93,8 @@ class SourceApiTests(unittest.TestCase):
detail = self.req('GET', f"/api/v1/businesses/{businesses[0]['id']}")[1]
self.assertTrue(detail['domains']); self.assertTrue(detail['websites']); self.assertTrue(detail['evidence'])
self.assertTrue(detail['contacts']); self.assertEqual(detail['review_status'], 'pending')
self.assertEqual(detail['score_version'], 'opportunity-v1')
self.assertTrue(all({'code', 'name', 'points', 'version'} <= set(item) for item in detail['score_factors']))
db = sqlite3.connect(self.tmp.name + '/x.db')
self.assertEqual(db.execute('SELECT processing_status FROM source_records').fetchone()[0], 'processed')
self.assertEqual(db.execute('SELECT status FROM enrichment_queue').fetchone()[0], 'completed')