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
128 lines
4.8 KiB
Python
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"}
|