All checks were successful
Deploy to Staging / Build Images (push) Successful in 35s
Deploy to Staging / Deploy to Staging (push) Successful in 53s
Deploy to Staging / Verify Staging (push) Successful in 9s
Deploy to Staging / Notify Staging Ready (push) Successful in 9s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Mirror Base Images / Mirror Base Images (push) Successful in 1m2s
123 lines
5.0 KiB
Python
123 lines
5.0 KiB
Python
"""Tests for deterministic VIN year resolution.
|
|
|
|
Covers: all three 30-year cycles (1980-2009, 2010-2039, 2040-2050),
|
|
position 7 disambiguation, edge cases, and invalid inputs.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch
|
|
from datetime import datetime
|
|
|
|
from app.engines.gemini_engine import resolve_vin_year
|
|
|
|
|
|
class TestSecondCycle:
|
|
"""Position 7 numeric -> 2010-2039 cycle."""
|
|
|
|
def test_p_with_numeric_pos7_returns_2023(self):
|
|
"""P=2023 when position 7 is numeric (the bug that triggered this fix)."""
|
|
# VIN: 1G1YE2D32P5602473 -- pos7='2' (numeric), pos10='P'
|
|
assert resolve_vin_year("1G1YE2D32P5602473") == 2023
|
|
|
|
def test_a_with_numeric_pos7_returns_2010(self):
|
|
"""A=2010 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE2112A5602473") == 2010
|
|
|
|
def test_l_with_numeric_pos7_returns_2020(self):
|
|
"""L=2020 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE2112L5602473") == 2020
|
|
|
|
def test_9_with_numeric_pos7_returns_2039(self):
|
|
"""9=2039 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE211295602473") == 2039
|
|
|
|
def test_digit_1_with_numeric_pos7_returns_2031(self):
|
|
"""1=2031 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE211215602473") == 2031
|
|
|
|
def test_s_with_numeric_pos7_returns_2025(self):
|
|
"""S=2025 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE2112S5602473") == 2025
|
|
|
|
def test_t_with_numeric_pos7_returns_2026(self):
|
|
"""T=2026 when position 7 is numeric."""
|
|
assert resolve_vin_year("1G1YE2112T5602473") == 2026
|
|
|
|
|
|
class TestFirstCycle:
|
|
"""Position 7 alphabetic -> 1980-2009 cycle (when 2040+ is not yet plausible)."""
|
|
|
|
def test_m_with_alpha_pos7_returns_1991(self):
|
|
"""M=1991 when position 7 is alphabetic (third cycle 2051 is not plausible)."""
|
|
assert resolve_vin_year("1G1YE2J32M5602473") == 1991
|
|
|
|
def test_n_with_alpha_pos7_returns_1992(self):
|
|
"""N=1992 when position 7 is alphabetic."""
|
|
assert resolve_vin_year("1G1YE2J32N5602473") == 1992
|
|
|
|
def test_p_with_alpha_pos7_returns_1993(self):
|
|
"""P=1993 when position 7 is alphabetic (third cycle 2053 not plausible)."""
|
|
assert resolve_vin_year("1G1YE2J32P5602473") == 1993
|
|
|
|
def test_y_with_alpha_pos7_returns_2000(self):
|
|
"""Y=2000 when position 7 is alphabetic."""
|
|
assert resolve_vin_year("1G1YE2J32Y5602473") == 2000
|
|
|
|
|
|
class TestThirdCycle:
|
|
"""Position 7 alphabetic + third cycle year (2040-2050) is plausible."""
|
|
|
|
@patch("app.engines.gemini_engine.datetime")
|
|
def test_a_with_alpha_pos7_returns_2040_when_plausible(self, mock_dt):
|
|
"""A=2040 when position 7 is alphabetic and year 2040 is plausible."""
|
|
mock_dt.now.return_value = datetime(2039, 1, 1)
|
|
# 2039 + 2 = 2041 >= 2040, so third cycle is plausible
|
|
assert resolve_vin_year("1G1YE2J32A5602473") == 2040
|
|
|
|
@patch("app.engines.gemini_engine.datetime")
|
|
def test_l_with_alpha_pos7_returns_2050_when_plausible(self, mock_dt):
|
|
"""L=2050 when position 7 is alphabetic and year 2050 is plausible."""
|
|
mock_dt.now.return_value = datetime(2049, 6, 1)
|
|
assert resolve_vin_year("1G1YE2J32L5602473") == 2050
|
|
|
|
@patch("app.engines.gemini_engine.datetime")
|
|
def test_a_with_alpha_pos7_returns_1980_when_2040_not_plausible(self, mock_dt):
|
|
"""A=1980 when third cycle year (2040) exceeds max plausible."""
|
|
mock_dt.now.return_value = datetime(2026, 2, 20)
|
|
# 2026 + 2 = 2028 < 2040, so third cycle not plausible -> first cycle
|
|
assert resolve_vin_year("1G1YE2J32A5602473") == 1980
|
|
|
|
@patch("app.engines.gemini_engine.datetime")
|
|
def test_k_with_alpha_pos7_returns_2049_when_plausible(self, mock_dt):
|
|
"""K=2049 when position 7 is alphabetic and year is plausible."""
|
|
mock_dt.now.return_value = datetime(2048, 1, 1)
|
|
assert resolve_vin_year("1G1YE2J32K5602473") == 2049
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Invalid inputs and boundary conditions."""
|
|
|
|
def test_short_vin_returns_none(self):
|
|
"""VIN shorter than 17 chars returns None."""
|
|
assert resolve_vin_year("1G1YE2D32") is None
|
|
|
|
def test_empty_vin_returns_none(self):
|
|
"""Empty string returns None."""
|
|
assert resolve_vin_year("") is None
|
|
|
|
def test_invalid_year_code_returns_none(self):
|
|
"""Position 10 with invalid code (e.g., 'Z') returns None."""
|
|
# Z is not a valid year code
|
|
assert resolve_vin_year("1G1YE2D32Z5602473") is None
|
|
|
|
def test_lowercase_vin_handled(self):
|
|
"""Lowercase VIN characters are handled correctly."""
|
|
assert resolve_vin_year("1g1ye2d32p5602473") == 2023
|
|
|
|
def test_i_o_q_not_valid_year_codes(self):
|
|
"""Letters I, O, Q are not valid VIN year codes."""
|
|
# These are excluded from VINs entirely but test graceful handling
|
|
assert resolve_vin_year("1G1YE2D32I5602473") is None
|
|
assert resolve_vin_year("1G1YE2D32O5602473") is None
|
|
assert resolve_vin_year("1G1YE2D32Q5602473") is None
|