# #region TestUpdateRunStatus [C:3] [TYPE Module] [SEMANTICS test,validation,run,status,counters] # @BRIEF Verify _update_run_status computes counters correctly, sets status to 'completed', # and handles edge cases (missing run, partial records, exception handling). # @RELATION BINDS_TO -> [LLMAnalysisPlugin] # @TEST_EDGE: missing_run_id -> Returns immediately without DB query # @TEST_EDGE: no_records_found -> Logs warning and returns # @TEST_EDGE: partial_records -> Does NOT mark complete when records < dashboard_count # @TEST_EDGE: exception_in_query -> Logs error, does not crash # @TEST_INVARIANT: run_status_always_completed -> VERIFIED_BY: test_sets_status_completed_with_all_records # @TEST_INVARIANT: counters_computed_from_records -> VERIFIED_BY: test_computed_counters_mixed_statuses, test_computed_counters_all_pass # @TEST_INVARIANT: finished_at_set_on_completion -> VERIFIED_BY: test_sets_finished_at_utc import sys from datetime import UTC, datetime, timezone from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Ensure src is importable (same pattern as test_llm_dashboard_validation_v2.py) sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from src.plugins.llm_analysis.plugin import _update_run_status # Minimal mock models matching the ORM shape class MockValidationRecord: def __init__(self, status, run_id="run-1"): self.status = status self.run_id = run_id class MockValidationRun: def __init__(self, id="run-1", dashboard_count=3, status="running"): self.id = id self.dashboard_count = dashboard_count self.status = status self.finished_at = None self.pass_count = 0 self.warn_count = 0 self.fail_count = 0 self.unknown_count = 0 def _make_mock_db(records=None, run=None): """Create a mock DB session that returns the given records and run.""" mock_db = MagicMock() mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.all.return_value = records or [] mock_query.first.return_value = run return mock_db # #region TestUpdateRunStatusNoop [C:2] [TYPE Class] # @BRIEF Verify _update_run_status early-exit conditions. class TestUpdateRunStatusNoop: """Verify _update_run_status no-op paths.""" # #region test_none_run_id_returns_early [C:2] [TYPE Function] # @BRIEF When run_id is None, function returns without DB access. def test_none_run_id_returns_early(self): mock_db = MagicMock() _update_run_status(mock_db, None) mock_db.query.assert_not_called() # #endregion test_none_run_id_returns_early # #region test_empty_run_id_returns_early [C:2] [TYPE Function] # @BRIEF When run_id is empty string, function returns without DB access. def test_empty_run_id_returns_early(self): mock_db = MagicMock() _update_run_status(mock_db, "") mock_db.query.assert_not_called() # #endregion test_empty_run_id_returns_early # #region test_no_records_logs_warning [C:2] [TYPE Function] # @BRIEF When no records exist for the run, logs warning and returns. def test_no_records_logs_warning(self): mock_db = _make_mock_db(records=[], run=MockValidationRun()) _update_run_status(mock_db, "run-1") mock_db.commit.assert_not_called() # #endregion test_no_records_logs_warning # #region test_no_run_found_logs_warning [C:2] [TYPE Function] # @BRIEF When run record not found, logs warning and returns. def test_no_run_found_logs_warning(self): mock_db = _make_mock_db(records=[MockValidationRecord("PASS")], run=None) _update_run_status(mock_db, "run-1") mock_db.commit.assert_not_called() # #endregion test_no_run_found_logs_warning # #endregion TestUpdateRunStatusNoop # #region TestUpdateRunStatusPartial [C:2] [TYPE Class] # @BRIEF Verify _update_run_status does NOT mark complete when records < dashboard_count. class TestUpdateRunStatusPartial: """Verify partial completion guard.""" # #region test_partial_records_not_marked_complete [C:2] [TYPE Function] # @BRIEF When len(records) < dashboard_count, run stays 'running'. def test_partial_records_not_marked_complete(self): run = MockValidationRun(dashboard_count=3, status="running") records = [MockValidationRecord("PASS"), MockValidationRecord("WARN")] # 2 < 3 mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.status == "running" # NOT changed mock_db.commit.assert_not_called() # #endregion test_partial_records_not_marked_complete # #region test_zero_dashboard_count_skips_guard [C:2] [TYPE Function] # @BRIEF When dashboard_count is 0 or None, the partial guard is skipped and run completes. def test_zero_dashboard_count_skips_guard(self): run = MockValidationRun(dashboard_count=0, status="running") records = [MockValidationRecord("PASS")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.status == "completed" mock_db.commit.assert_called_once() # #endregion test_zero_dashboard_count_skips_guard # #endregion TestUpdateRunStatusPartial # #region TestUpdateRunStatusCompletion [C:2] [TYPE Class] # @BRIEF Verify _update_run_status correctly computes counters and sets status. class TestUpdateRunStatusCompletion: """Verify counter computation and status assignment.""" # #region test_sets_status_completed_with_all_records [C:2] [TYPE Function] # @BRIEF When all expected records exist, run.status is set to 'completed'. def test_sets_status_completed_with_all_records(self): run = MockValidationRun(dashboard_count=2, status="running") records = [MockValidationRecord("PASS"), MockValidationRecord("FAIL")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.status == "completed" # #endregion test_sets_status_completed_with_all_records # #region test_computed_counters_all_pass [C:2] [TYPE Function] # @BRIEF All PASS records → pass_count=2, others=0. def test_computed_counters_all_pass(self): run = MockValidationRun(dashboard_count=2) records = [MockValidationRecord("PASS"), MockValidationRecord("PASS")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.pass_count == 2 assert run.warn_count == 0 assert run.fail_count == 0 assert run.unknown_count == 0 # #endregion test_computed_counters_all_pass # #region test_computed_counters_mixed_statuses [C:2] [TYPE Function] # @BRIEF Mixed statuses → correct per-status counters. def test_computed_counters_mixed_statuses(self): run = MockValidationRun(dashboard_count=4) records = [ MockValidationRecord("PASS"), MockValidationRecord("PASS"), MockValidationRecord("WARN"), MockValidationRecord("FAIL"), ] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.pass_count == 2 assert run.warn_count == 1 assert run.fail_count == 1 assert run.unknown_count == 0 # #endregion test_computed_counters_mixed_statuses # #region test_computed_counters_unknown_status [C:2] [TYPE Function] # @BRIEF UNKNOWN status records are counted correctly. def test_computed_counters_unknown_status(self): run = MockValidationRun(dashboard_count=2) records = [MockValidationRecord("UNKNOWN"), MockValidationRecord("PASS")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.unknown_count == 1 assert run.pass_count == 1 # #endregion test_computed_counters_unknown_status # #region test_sets_finished_at [C:2] [TYPE Function] # @BRIEF finished_at is set to a datetime on completion. def test_sets_finished_at(self): run = MockValidationRun(dashboard_count=1, status="running") records = [MockValidationRecord("PASS")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") assert run.finished_at is not None assert isinstance(run.finished_at, datetime) # #endregion test_sets_finished_at # #region test_db_exception_logged_not_raised [C:2] [TYPE Function] # @BRIEF DB exception during finalization is logged but not re-raised. def test_db_exception_logged_not_raised(self): mock_db = MagicMock() mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.all.side_effect = RuntimeError("DB connection lost") # Should NOT raise _update_run_status(mock_db, "run-1") # #endregion test_db_exception_logged_not_raised # #endregion TestUpdateRunStatusCompletion # #region TestExecuteErrorHandling [C:2] [TYPE Class] # @BRIEF Verify the always-finalize pattern used in execute(). class TestExecuteErrorHandling: """Verify _update_run_status is always callable after the loop.""" # #region test_update_run_status_called_on_success [C:2] [TYPE Function] # @BRIEF _update_run_status is called after normal execution with all records. def test_update_run_status_called_on_success(self): run = MockValidationRun(dashboard_count=1, status="running") records = [MockValidationRecord("PASS")] mock_db = _make_mock_db(records=records, run=run) _update_run_status(mock_db, "run-1") mock_db.commit.assert_called_once() assert run.status == "completed" # #endregion test_update_run_status_called_on_success # #region test_update_run_status_handles_zero_records [C:2] [TYPE Function] # @BRIEF When all dashboards failed (0 records), _update_run_status still runs # and does not mark the run as complete (partial guard). def test_update_run_status_handles_zero_records(self): run = MockValidationRun(dashboard_count=2, status="running") mock_db = _make_mock_db(records=[], run=run) _update_run_status(mock_db, "run-1") # With 0 records and dashboard_count=2, the partial guard prevents completion assert run.status == "running" mock_db.commit.assert_not_called() # #endregion test_update_run_status_handles_zero_records # #endregion TestExecuteErrorHandling # #endregion TestUpdateRunStatus