Files
geointel/backend/tests/test_quality_evidence_bounds.py
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

128 lines
4.8 KiB
Python

"""Evidence review must stay usable on a regional run.
evidence_geojson emitted one feature per false positive, one per false
negative and *two* per match, with no limit. A regional QA run of 40k
detections against 45k reference footprints produced well over a hundred
thousand features in a single response, plus one warning string per
unresolvable identifier. The endpoint the whole review workflow depends on
therefore stopped working exactly where review matters most.
The budget goes to what a reviewer must act on — misses and false positives —
before confirmations, and the response says what it left out.
"""
from __future__ import annotations
from app.services.quality_evidence_service import QualityEvidenceService
def _features(role: str, count: int) -> list[dict]:
return [{"properties": {"evidence_role": role}, "id": f"{role}-{index}"} for index in range(count)]
def test_missing_identifiers_collapse_into_one_statement() -> None:
warnings = QualityEvidenceService.summarize_missing(
candidate_ids=["a", "b", "c"],
reference_ids=["r1"],
)
assert len(warnings) == 1
assert "3" in warnings[0]
assert "1" in warnings[0]
def test_nothing_missing_produces_no_warning() -> None:
assert QualityEvidenceService.summarize_missing(candidate_ids=[], reference_ids=[]) == []
def test_the_plan_is_capped_before_any_geometry_is_fetched() -> None:
"""Resolving 130k geometries to draw 5k of them is work for nothing."""
findings = {
"match_evidence": [{"candidate_feature_id": f"c{i}", "reference_feature_id": f"r{i}"} for i in range(100)],
"false_positive_evidence": [{"candidate_feature_id": f"fp{i}"} for i in range(10)],
"false_negative_evidence": [{"reference_feature_id": f"fn{i}"} for i in range(10)],
}
plan = QualityEvidenceService.plan_evidence(findings, limit=8)
assert plan.truncated is True
assert len(plan.items) == 8
# Both error classes are represented; confirmations do not get a share
# while errors are still waiting.
assert {item.role for item in plan.items} == {"false_negative", "false_positive"}
# Only the identifiers that will actually be drawn need resolving.
assert len(plan.candidate_ids) + len(plan.reference_ids) == 8
assert plan.candidate_ids <= {f"fp{i}" for i in range(10)}
assert plan.reference_ids <= {f"fn{i}" for i in range(10)}
def test_a_rare_error_class_is_never_crowded_out() -> None:
"""50.000 misses must not hide the three false positives."""
findings = {
"false_negative_evidence": [{"reference_feature_id": f"fn{i}"} for i in range(5_000)],
"false_positive_evidence": [{"candidate_feature_id": f"fp{i}"} for i in range(3)],
}
plan = QualityEvidenceService.plan_evidence(findings, limit=100)
roles = [item.role for item in plan.items]
assert roles.count("false_positive") >= 1
assert roles.count("false_negative") >= 90
assert len(plan.items) == 100
def test_the_plan_reports_the_complete_population_not_the_capped_one() -> None:
findings = {
"match_evidence": [{"candidate_feature_id": f"c{i}", "reference_feature_id": f"r{i}"} for i in range(100)],
"false_negative_evidence": [{"reference_feature_id": "fn"}],
}
plan = QualityEvidenceService.plan_evidence(findings, limit=2)
assert plan.total_feature_count == 201
assert plan.role_counts == {"match_candidate": 100, "match_reference": 100, "false_negative": 1}
def test_an_uncapped_plan_keeps_everything() -> None:
findings = {"false_negative_evidence": [{"reference_feature_id": f"fn{i}"} for i in range(30)]}
plan = QualityEvidenceService.plan_evidence(findings, limit=0)
assert plan.truncated is False
assert len(plan.items) == 30
assert plan.reference_ids == {f"fn{i}" for i in range(30)}
def test_evidence_without_identifiers_is_skipped_not_planned() -> None:
findings = {
"false_positive_evidence": [{"candidate_feature_id": None}, {"candidate_feature_id": "fp"}],
"false_negative_evidence": [{}],
}
plan = QualityEvidenceService.plan_evidence(findings, limit=0)
assert [item.role for item in plan.items] == ["false_positive"]
assert plan.candidate_ids == {"fp"}
def test_a_planned_match_keeps_its_candidate_and_reference_together() -> None:
"""Half a match is not reviewable evidence."""
findings = {
"match_evidence": [
{"candidate_feature_id": "c1", "reference_feature_id": "r1"},
{"candidate_feature_id": "c2", "reference_feature_id": "r2"},
]
}
plan = QualityEvidenceService.plan_evidence(findings, limit=3)
assert plan.truncated is True
# An odd budget drops the second pair rather than showing one side of it.
assert len(plan.items) == 1
assert plan.candidate_ids == {"c1"}
assert plan.reference_ids == {"r1"}