diff --git a/backend/apps/ifc_validation/tests/tests_header_syntax_validation_task.py b/backend/apps/ifc_validation/tests/tests_header_syntax_validation_task.py index 6a83fdbd..b637ff2c 100644 --- a/backend/apps/ifc_validation/tests/tests_header_syntax_validation_task.py +++ b/backend/apps/ifc_validation/tests/tests_header_syntax_validation_task.py @@ -96,8 +96,8 @@ def test_determine_aggregate_status_for_multiple_outcomes(self): 'output': Model.Status.INVALID }, { - 'input': [], - 'output': Model.Status.VALID + 'input': [], + 'output': Model.Status.NOT_VALIDATED } ] diff --git a/backend/apps/ifc_validation/tests/tests_premature_green.py b/backend/apps/ifc_validation/tests/tests_premature_green.py new file mode 100644 index 00000000..3b1dfceb --- /dev/null +++ b/backend/apps/ifc_validation/tests/tests_premature_green.py @@ -0,0 +1,144 @@ +"""No UI status cell may show GREEN ('v') before a contributing check has actually +passed — i.e. it must never be green *before it turns red*. This covers the +'skipped shows green' bug + the syntax in-progress flash. The assertions below replicate +the cell expressions in apps/ifc_validation_bff/views_legacy.py format_request() exactly; +keep them in sync with that file (status_schema / status_rules / status_syntax). + +Execution order (apps/ifc_validation/tasks/configs.py + task_runner.py): serial +magic_clamav -> header_syntax -> header -> syntax -> prerequisites, then parallel +schema, signatures, IA, IP, industry. So earlier tasks finish while later siblings are +still pending = the window where a premature green could appear. +""" +from django.test import TestCase +from django.contrib.auth.models import User + +from apps.ifc_validation_models.models import ( + Model, ValidationRequest, ValidationTask, ValidationOutcome, set_user_context, +) +from apps.ifc_validation_bff.status import status_combine + +S = ValidationOutcome.OutcomeSeverity +T = ValidationTask.Type +St = Model.Status + + +# ---- exact replicas of the BFF cell assembly (views_legacy.py format_request) ---- +def ui_schema(m): + return status_combine( + "p" if m.status_schema_calculated is None else m.status_schema_calculated, + "p" if m.status_prereq is None else m.status_prereq) + +def ui_rules(m): + return status_combine( + "p" if m.status_ia_calculated is None else m.status_ia_calculated, + "p" if m.status_ip_calculated is None else m.status_ip_calculated) + +def ui_syntax(m, completed): + # allow_not_executed is gated on request COMPLETED (see views_legacy.py) + return status_combine( + "p" if m.status_syntax is None else m.status_syntax, + "p" if m.status_header_syntax is None else m.status_header_syntax, + allow_not_executed=completed) + + +class NoPrematureGreenTestCase(TestCase): + + @staticmethod + def _user(): + u, _ = User.objects.get_or_create(id=1, defaults={'username': 'SYSTEM', 'is_active': True}) + set_user_context(u) + return u + + def _model(self): + u = self._user() + m = Model.objects.create(file_name='m.ifc', size=1, uploaded_by=u) + r = ValidationRequest.objects.create(file_name='m.ifc', file='m.ifc', size=1) + r.model = m; r.save() + self.req = r + return m + + def _task(self, ttype, severities): + t = ValidationTask.objects.create(request=self.req, type=ttype) + for s in severities: + ValidationOutcome.objects.create(validation_task=t, severity=s) + return t + + def _fresh(self, m): + return Model.objects.get(id=m.id) # per-request fresh fetch (clears cached_property) + + # ===== SCHEMA — the original "green after IFC101" report ===== + def test_schema_inprogress_after_prereq_pass_is_not_green(self): + m = self._model() + m.status_prereq = St.VALID; m.save() # IFC101/prereq passed + self._task(T.SCHEMA, []) # schema task created, not yet run + self.assertEqual(ui_schema(self._fresh(m)), St.NOT_VALIDATED) + + def test_schema_invalid_is_red(self): + m = self._model() + m.status_prereq = St.VALID; m.save() + self._task(T.SCHEMA, [S.PASSED, S.ERROR]) + self.assertEqual(ui_schema(self._fresh(m)), St.INVALID) + + def test_schema_green_only_when_passed(self): + m = self._model() + m.status_prereq = St.VALID; m.save() + self._task(T.SCHEMA, [S.PASSED]) + self.assertEqual(ui_schema(self._fresh(m)), St.VALID) + + # ===== RULES (IA + IP) ===== + def test_rules_inprogress_one_pending_is_not_green(self): + m = self._model() + self._task(T.NORMATIVE_IA, [S.PASSED]) # IA passed + self._task(T.NORMATIVE_IP, []) # IP still pending + self.assertEqual(ui_rules(self._fresh(m)), St.NOT_VALIDATED) + + def test_rules_invalid_is_red(self): + m = self._model() + self._task(T.NORMATIVE_IA, [S.PASSED]) + self._task(T.NORMATIVE_IP, [S.PASSED, S.ERROR]) + self.assertEqual(ui_rules(self._fresh(m)), St.INVALID) + + def test_rules_green_only_when_both_pass(self): + m = self._model() + self._task(T.NORMATIVE_IA, [S.PASSED]) + self._task(T.NORMATIVE_IP, [S.PASSED]) + self.assertEqual(ui_rules(self._fresh(m)), St.VALID) + + # ===== RAW single-task cells: pending = default 'n' (grey), never green ===== + def test_raw_cells_default_not_validated_while_pending(self): + m = self._model() + for field in ['status_bsdd', 'status_header', 'status_industry_practices', + 'status_signatures', 'status_magic_clamav', 'status_mvd', 'status_ids']: + self.assertEqual(getattr(m, field), St.NOT_VALIDATED) + + # ===== SYNTAX — completed-gated allow_not_executed (fixes the in-progress flash) ===== + def test_syntax_inprogress_after_header_syntax_pass_is_not_green(self): + # header_syntax finishes before syntax (serial); mid-run the strip is OFF so the + # pending syntax 'n' is kept -> grey, not a premature green. + m = self._model() + m.status_header_syntax = St.VALID + m.status_syntax = St.NOT_VALIDATED + m.save() + self.assertEqual(ui_syntax(m, completed=False), St.NOT_VALIDATED) + + def test_syntax_invalid_is_red(self): + m = self._model() + m.status_header_syntax = St.VALID + m.status_syntax = St.INVALID + m.save() + self.assertEqual(ui_syntax(m, completed=False), St.INVALID) + + def test_syntax_legacy_completed_strips_not_executed_header_syntax(self): + # legacy COMPLETED file: header_syntax never ran ('n'), syntax passed ('v') -> 'v' + m = self._model() + m.status_header_syntax = St.NOT_VALIDATED + m.status_syntax = St.VALID + m.save() + self.assertEqual(ui_syntax(m, completed=True), St.VALID) + + def test_syntax_completed_all_skipped_stays_not_validated(self): + m = self._model() + m.status_header_syntax = St.NOT_VALIDATED + m.status_syntax = St.NOT_VALIDATED + m.save() + self.assertEqual(ui_syntax(m, completed=True), St.NOT_VALIDATED) diff --git a/backend/apps/ifc_validation/tests/tests_status_combine.py b/backend/apps/ifc_validation/tests/tests_status_combine.py index 849324ac..8e7c95c7 100644 --- a/backend/apps/ifc_validation/tests/tests_status_combine.py +++ b/backend/apps/ifc_validation/tests/tests_status_combine.py @@ -29,3 +29,9 @@ def test_allow_not_executed_filters_n_when_mixed(self): def test_allow_not_executed_keeps_n_when_all_n(self): self.assertEqual(status_combine('n', 'n', allow_not_executed=True), 'n') + + def test_not_validated_outranks_valid_when_not_stripped(self): + # without allow_not_executed, a pending/not-validated 'n' must beat a sibling 'v' + # (n=3 > v=2 in "-pvnwi") so schema/rules never show green while a check is pending + self.assertEqual(status_combine('n', 'v'), 'n') + self.assertEqual(status_combine('v', 'n'), 'n') diff --git a/backend/apps/ifc_validation/tests/tests_syntax_validation_task.py b/backend/apps/ifc_validation/tests/tests_syntax_validation_task.py index 68a90b7e..eb569c68 100644 --- a/backend/apps/ifc_validation/tests/tests_syntax_validation_task.py +++ b/backend/apps/ifc_validation/tests/tests_syntax_validation_task.py @@ -120,8 +120,8 @@ def test_determine_aggregate_status_for_multiple_outcomes(self): 'output': Model.Status.INVALID }, { - 'input': [], - 'output': Model.Status.VALID + 'input': [], + 'output': Model.Status.NOT_VALIDATED } ] diff --git a/backend/apps/ifc_validation_bff/views_legacy.py b/backend/apps/ifc_validation_bff/views_legacy.py index f5b671c9..bbe423ef 100644 --- a/backend/apps/ifc_validation_bff/views_legacy.py +++ b/backend/apps/ifc_validation_bff/views_legacy.py @@ -166,7 +166,10 @@ def format_request(request : ValidationRequest): "status_syntax": status_combine( "p" if (request.model is None or request.model.status_syntax is None) else request.model.status_syntax, "p" if (request.model is None or request.model.status_header_syntax is None) else request.model.status_header_syntax, - allow_not_executed=True + # Only strip a not-executed 'n' once the run is COMPLETED (legacy files + # where header_syntax never ran). Mid-run, header_syntax finishes before + # syntax, so stripping the pending syntax 'n' would flash green before red. + allow_not_executed=(request.status == ValidationRequest.Status.COMPLETED) ), "status_schema": status_combine( "p" if (request.model is None or request.model.status_schema_calculated is None) else request.model.status_schema_calculated, diff --git a/backend/apps/ifc_validation_models b/backend/apps/ifc_validation_models index 957298d0..8fbf3edb 160000 --- a/backend/apps/ifc_validation_models +++ b/backend/apps/ifc_validation_models @@ -1 +1 @@ -Subproject commit 957298d03fff4fefb62574f358c66db65f0f29fb +Subproject commit 8fbf3edbd9c18904adb3c493a3c4090bbadcba65