157 lines
6.2 KiB
Python
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)
|