js - ts + fix
This commit is contained in:
256
backend/tests/plugins/test_update_run_status.py
Normal file
256
backend/tests/plugins/test_update_run_status.py
Normal file
@@ -0,0 +1,256 @@
|
||||
# #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
|
||||
Reference in New Issue
Block a user