From 30ca709c11c41c749e40f5bb23042ee9d384b6ff Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 18 Jun 2026 17:05:15 +0200 Subject: [PATCH] fix(coverage): partial coverage when mix of detected+not_detected; add bulk recalculate endpoint --- backend/app/domain/entities/technique.py | 8 +++--- backend/app/routers/system.py | 32 ++++++++++++++++++++++++ backend/tests/test_technique_entity.py | 12 +++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/backend/app/domain/entities/technique.py b/backend/app/domain/entities/technique.py index 1395596..70c9529 100644 --- a/backend/app/domain/entities/technique.py +++ b/backend/app/domain/entities/technique.py @@ -271,13 +271,13 @@ class TechniqueEntity: else: self.status_global = TechniqueStatus.partial elif any( - # Keyword argument: r - r == TestResult.partially_detected or r == "partially_detected" + r in (TestResult.detected, "detected", + TestResult.partially_detected, "partially_detected") for r in results ): - # Assign self.status_global = TechniqueStatus.partial + # Mix of detected + not_detected, or any partially_detected → partial self.status_global = TechniqueStatus.partial - # Fallback: handle remaining cases + # Fallback: handle remaining cases (all not_detected) else: # Assign self.status_global = TechniqueStatus.not_covered self.status_global = TechniqueStatus.not_covered diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py index 4337d77..16aa1f4 100644 --- a/backend/app/routers/system.py +++ b/backend/app/routers/system.py @@ -824,6 +824,38 @@ def re_enrich_evaluation_round( return summary +@router.post("/recalculate-coverage") +def recalculate_all_coverage( + db: Session = Depends(get_db), + current_user: User = Depends(require_role("admin")), +): + """Recompute status_global for every technique based on its current tests. + + **Requires** the ``admin`` role. + Run this after logic changes to the coverage scoring rules. + """ + from app.models.technique import Technique + from app.services.status_service import recalculate_technique_status + + techniques = db.query(Technique).all() + updated = 0 + for tech in techniques: + old_status = tech.status_global + recalculate_technique_status(db, tech) + if tech.status_global != old_status: + updated += 1 + db.commit() + logger.info( + "Bulk coverage recalculation: %d/%d techniques updated (by %s)", + updated, len(techniques), current_user.email, + ) + return { + "total": len(techniques), + "updated": updated, + "message": f"Coverage recalculated for {len(techniques)} techniques. {updated} statuses changed.", + } + + @router.post("/email-test") def send_test_email( payload: EmailTestRequest, diff --git a/backend/tests/test_technique_entity.py b/backend/tests/test_technique_entity.py index 3b749d7..432acb2 100644 --- a/backend/tests/test_technique_entity.py +++ b/backend/tests/test_technique_entity.py @@ -135,6 +135,18 @@ class TestRecalculateStatus: tests = [("validated", "not_detected"), ("validated", "not_detected")] assert e.recalculate_status(tests) == TechniqueStatus.not_covered + def test_all_validated_mix_detected_and_not_detected_gives_partial(self): + e = _entity() + tests = [ + ("validated", "detected"), + ("validated", "detected"), + ("validated", "detected"), + ("validated", "detected"), + ("validated", "detected"), + ("validated", "not_detected"), + ] + assert e.recalculate_status(tests) == TechniqueStatus.partial + def test_all_validated_no_results_gives_not_covered(self): e = _entity() tests = [("validated", None)]