Files
MobilityOps/backend/tests/test_vehicle_status.py
T

157 lines
6.2 KiB
Python

"""Pure unit tests for the shared vehicle-status evaluator -- no database needed, since
evaluate_vehicle_status() only reasons over an already-gathered VehicleStatusFacts. See
docs/fleet-ops-correction/vehicle-status-decision-table.md for the decision table these
tests are asserting against."""
from types import SimpleNamespace
from app.services.vehicle_status import (
RECOMMENDATION_CODE_ACTIVE_RENTAL,
RECOMMENDATION_CODE_BOOKING_CONFLICT,
RECOMMENDATION_CODE_MANUAL_REVIEW,
RECOMMENDATION_CODE_NO_CONFLICT,
RECOMMENDATION_CODE_RENTAL_ENDED,
RECOMMENDATION_CODE_SERVICE_THRESHOLD,
VehicleStatusFacts,
compute_recommendation_token,
evaluate_vehicle_status,
)
def _vehicle(status: str, *, version: int = 1):
return SimpleNamespace(operational_status=status, version=version)
def _facts(**overrides) -> VehicleStatusFacts:
defaults = dict(
active_booking_refs=[],
overlapping_booking_pairs=[],
service_threshold_reached=False,
odometer_km=10_000,
next_service_km=20_000,
open_booking_overlap_issue_ref=None,
)
defaults.update(overrides)
return VehicleStatusFacts(**defaults)
def test_available_with_active_rental_recommends_rented():
result = evaluate_vehicle_status(_vehicle("available"), _facts(active_booking_refs=["BK-0001"]))
assert result.recommended_status == "rented"
assert result.recommendation_code == RECOMMENDATION_CODE_ACTIVE_RENTAL
assert result.safe_to_apply is True
assert result.manual_review_required is False
def test_maintenance_with_active_rental_never_auto_recommends_rented():
# The exact unsafe shortcut this task explicitly forbids: maintenance + an active
# booking must NEVER be auto-resolved to "rented".
result = evaluate_vehicle_status(
_vehicle("maintenance"), _facts(active_booking_refs=["BK-0001"])
)
assert result.recommended_status is None
assert result.manual_review_required is True
assert result.safe_to_apply is False
assert result.recommendation_code == RECOMMENDATION_CODE_MANUAL_REVIEW
def test_available_with_active_rental_and_service_threshold_requires_manual_review():
result = evaluate_vehicle_status(
_vehicle("available"),
_facts(active_booking_refs=["BK-0001"], service_threshold_reached=True),
)
assert result.manual_review_required is True
assert result.recommended_status is None
def test_available_with_active_rental_and_booking_conflict_requires_manual_review():
result = evaluate_vehicle_status(
_vehicle("available"),
_facts(active_booking_refs=["BK-0001"], open_booking_overlap_issue_ref="DQ-0001"),
)
assert result.manual_review_required is True
assert result.recommended_status is None
def test_rented_with_no_active_booking_recommends_available():
result = evaluate_vehicle_status(_vehicle("rented"), _facts())
assert result.recommended_status == "available"
assert result.recommendation_code == RECOMMENDATION_CODE_RENTAL_ENDED
assert result.safe_to_apply is True
def test_service_threshold_reached_recommends_maintenance():
result = evaluate_vehicle_status(_vehicle("available"), _facts(service_threshold_reached=True))
assert result.recommended_status == "maintenance"
assert result.recommendation_code == RECOMMENDATION_CODE_SERVICE_THRESHOLD
def test_booking_conflict_recommends_blocked_not_a_generic_high_severity_proxy():
# The evaluator must react to a *real* booking-conflict fact, not "does some other
# open high-severity issue happen to exist" (the forbidden proxy).
result = evaluate_vehicle_status(
_vehicle("available"),
_facts(overlapping_booking_pairs=[("BK-DEMO-OVERLAP-A", "BK-DEMO-OVERLAP-B")]),
)
assert result.recommended_status == "blocked"
assert result.recommendation_code == RECOMMENDATION_CODE_BOOKING_CONFLICT
def test_open_booking_overlap_issue_alone_also_triggers_blocked():
result = evaluate_vehicle_status(
_vehicle("available"), _facts(open_booking_overlap_issue_ref="DQ-DEMO-OVERLAP")
)
assert result.recommended_status == "blocked"
assert result.recommendation_code == RECOMMENDATION_CODE_BOOKING_CONFLICT
def test_maintenance_with_no_active_rental_and_no_blockers_stays_manual():
# No fact here confirms maintenance is actually finished, so the evaluator must not
# auto-clear it to "available" -- that release remains an explicit, manual decision.
result = evaluate_vehicle_status(_vehicle("maintenance"), _facts())
assert result.recommended_status is None
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
assert result.safe_to_apply is False
def test_no_conflict_when_status_already_matches_facts():
result = evaluate_vehicle_status(_vehicle("available"), _facts())
assert result.recommended_status is None
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
assert result.safe_to_apply is False
result = evaluate_vehicle_status(_vehicle("rented"), _facts(active_booking_refs=["BK-1"]))
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
result = evaluate_vehicle_status(_vehicle("blocked"), _facts())
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
result = evaluate_vehicle_status(
_vehicle("maintenance"), _facts(service_threshold_reached=True)
)
assert result.recommendation_code == RECOMMENDATION_CODE_NO_CONFLICT
def test_recommendation_token_changes_when_facts_change():
vehicle = _vehicle("available")
facts_a = _facts()
facts_b = _facts(service_threshold_reached=True)
assert compute_recommendation_token(vehicle, facts_a) != compute_recommendation_token(
vehicle, facts_b
)
def test_recommendation_token_is_stable_for_identical_facts():
vehicle = _vehicle("available")
facts = _facts(active_booking_refs=["BK-0001"])
assert compute_recommendation_token(vehicle, facts) == compute_recommendation_token(
vehicle, facts
)
def test_recommendation_token_changes_when_vehicle_version_changes():
facts = _facts()
assert compute_recommendation_token(
_vehicle("available", version=1), facts
) != compute_recommendation_token(_vehicle("available", version=2), facts)