test: cover review-gated entity resolution
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
from services.api.workers.entity_resolution import (
|
||||
EntityCandidate,
|
||||
ResolutionDecision,
|
||||
rank_resolution_candidates,
|
||||
resolve_entity_pair,
|
||||
)
|
||||
|
||||
|
||||
def organization(entity_id: str, **overrides):
|
||||
values = {
|
||||
"entity_id": entity_id,
|
||||
"entity_type": "organization",
|
||||
"name": "Example Technology AB",
|
||||
"source_ref": f"source:{entity_id}",
|
||||
"registration_country": "SE",
|
||||
"registration_number": None,
|
||||
"website": None,
|
||||
"city": "Stockholm",
|
||||
}
|
||||
values.update(overrides)
|
||||
return EntityCandidate(**values)
|
||||
|
||||
|
||||
def person(entity_id: str, **overrides):
|
||||
values = {
|
||||
"entity_id": entity_id,
|
||||
"entity_type": "person",
|
||||
"name": "Anna Andersson",
|
||||
"source_ref": f"source:{entity_id}",
|
||||
"email": None,
|
||||
"organization_id": None,
|
||||
}
|
||||
values.update(overrides)
|
||||
return EntityCandidate(**values)
|
||||
|
||||
|
||||
def test_registration_number_is_a_strong_organization_identifier():
|
||||
result = resolve_entity_pair(
|
||||
organization("left", registration_number="556123-4567"),
|
||||
organization("right", registration_number="5561234567"),
|
||||
)
|
||||
assert result.decision is ResolutionDecision.EXACT_LINK
|
||||
assert result.score == 1.0
|
||||
assert result.requires_human_review is False
|
||||
|
||||
|
||||
def test_conflicting_registration_numbers_fail_distinct_even_when_names_match():
|
||||
result = resolve_entity_pair(
|
||||
organization("left", registration_number="5561234567"),
|
||||
organization("right", registration_number="5569999999"),
|
||||
)
|
||||
assert result.decision is ResolutionDecision.DISTINCT
|
||||
assert result.score == 0.0
|
||||
|
||||
|
||||
def test_same_person_email_is_exact_link():
|
||||
result = resolve_entity_pair(
|
||||
person("left", email="Anna@example.com"),
|
||||
person("right", email=" anna@EXAMPLE.com "),
|
||||
)
|
||||
assert result.decision is ResolutionDecision.EXACT_LINK
|
||||
assert result.requires_human_review is False
|
||||
|
||||
|
||||
def test_fuzzy_organization_similarity_never_auto_merges():
|
||||
result = resolve_entity_pair(
|
||||
organization("left", name="Example Technology AB", website="example.se"),
|
||||
organization("right", name="Example Technologies AB", website="https://www.example.se"),
|
||||
)
|
||||
assert result.score > 0.72
|
||||
assert result.decision is ResolutionDecision.REVIEW
|
||||
assert result.requires_human_review is True
|
||||
|
||||
|
||||
def test_different_entity_types_are_distinct():
|
||||
result = resolve_entity_pair(
|
||||
organization("org"),
|
||||
person("person"),
|
||||
)
|
||||
assert result.decision is ResolutionDecision.DISTINCT
|
||||
|
||||
|
||||
def test_candidate_ranking_prefers_exact_then_review_and_is_stable():
|
||||
query = organization("query", registration_number="5561234567", website="example.se")
|
||||
candidates = [
|
||||
organization("weak", name="Completely Different Company", registration_country="NO", city="Oslo"),
|
||||
organization("review", name="Example Technologies AB", website="example.se"),
|
||||
organization("exact", registration_number="556123-4567"),
|
||||
]
|
||||
ranked = rank_resolution_candidates(query, candidates)
|
||||
assert [result.right_id for result in ranked] == ["exact", "review", "weak"]
|
||||
assert ranked[0].decision is ResolutionDecision.EXACT_LINK
|
||||
assert ranked[1].decision is ResolutionDecision.REVIEW
|
||||
Reference in New Issue
Block a user