From 37d8a3702fa8e8b4daab84ee0426ec60766d5978 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 2 Sep 2026 16:46:14 +0300 Subject: [PATCH] Add tests/test_models.py --- tests/test_models.py | 215 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/test_models.py diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..ca07ce3 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,215 @@ +"""Unit-тесты pydantic-моделей данных.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from decimal import Decimal + +import pytest +from pydantic import ValidationError + +from selectel_ml_tui.models import ( + Consumption, + ConsumptionItem, + HistoryPoint, + Metrics, + ModelMetrics, + ProjectRef, + format_percent, + format_rub, + format_tokens, +) + + +class TestConsumption: + def test_parse_with_z_timestamp(self) -> None: + item = Consumption.model_validate( + { + "period": "2026-08-01T00:00:00.000Z", + "amount_rub": "854.83", + "input_tokens": 100, + "output_tokens": 50, + "cache_read_tokens": 10, + "cache_write_tokens": 5, + } + ) + assert item.period == datetime(2026, 8, 1, tzinfo=timezone.utc) + assert item.amount_rub == Decimal("854.83") + assert item.input_tokens == 100 + + def test_parse_naive_timestamp(self) -> None: + item = Consumption.model_validate({"period": "2026-08-01T00:00:00"}) + assert item.period == datetime(2026, 8, 1) + assert item.amount_rub == Decimal("0") + assert item.input_tokens == 0 + + def test_negative_amount_rejected(self) -> None: + with pytest.raises(ValidationError): + Consumption.model_validate( + {"period": "2026-08-01T00:00:00", "amount_rub": "-1"} + ) + + def test_negative_tokens_rejected(self) -> None: + with pytest.raises(ValidationError): + Consumption.model_validate( + { + "period": "2026-08-01T00:00:00", + "input_tokens": -5, + } + ) + + def test_invalid_date_rejected(self) -> None: + with pytest.raises(ValidationError): + Consumption.model_validate({"period": "not-a-date"}) + + +class TestMetrics: + def test_parse_metrics(self) -> None: + metrics = Metrics.model_validate( + { + "availability_pct": 99.912, + "avg_ttft_ms": 0.45, + "input_tokens": 1000, + "output_tokens": 900, + "cache_read_tokens": 800, + "cache_write_tokens": 700, + } + ) + assert metrics.availability_pct == 99.912 + assert metrics.avg_ttft_ms == 0.45 + assert metrics.input_tokens == 1000 + + def test_missing_fields_defaults(self) -> None: + metrics = Metrics.model_validate({}) + assert metrics.availability_pct == 100.0 + assert metrics.input_tokens == 0 + assert metrics.avg_ttft_ms == 0.0 + + def test_availability_bounds_rejected(self) -> None: + with pytest.raises(ValidationError): + Metrics.model_validate({"availability_pct": 101}) + with pytest.raises(ValidationError): + Metrics.model_validate({"availability_pct": -1}) + + +class TestModelMetrics: + def test_parse_model_metrics(self) -> None: + model = ModelMetrics.model_validate( + { + "model_id": "gpt-4o", + "model_name": "GPT-4o", + "availability_pct": 99.5, + "avg_ttft_ms": 0.3, + } + ) + assert model.model_id == "gpt-4o" + assert model.model_name == "GPT-4o" + assert model.availability_pct == 99.5 + + +class TestHistoryPoint: + def test_parse(self) -> None: + point = HistoryPoint.model_validate( + { + "timestamp": "2026-08-01T12:00:00Z", + "amount_rub": "12.5", + "tokens": { + "input_tokens": 100, + "output_tokens": 200, + "cache_read_tokens": 30, + "cache_write_tokens": 40, + }, + } + ) + assert point.timestamp == datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc) + assert point.amount_rub == Decimal("12.5") + assert point.tokens.output_tokens == 200 + + def test_missing_tokens_defaults(self) -> None: + point = HistoryPoint.model_validate({"timestamp": "2026-08-01T00:00:00"}) + assert point.tokens.input_tokens == 0 + assert point.amount_rub == Decimal("0") + + +class TestConsumptionItem: + def _base(self, **overrides: object) -> dict[str, object]: + data: dict[str, object] = { + "account_id": "239633", + "period": "2026-08-01T00:00:00", + "provider_key": "aig", + "value": 854.83, + } + data.update(overrides) + return data + + def test_project_as_string(self) -> None: + item = ConsumptionItem.model_validate( + self._base(project="7bf2d2a328494e25b13d05a1926ca75a") + ) + assert item.project_id == "7bf2d2a328494e25b13d05a1926ca75a" + + def test_project_as_object(self) -> None: + item = ConsumptionItem.model_validate( + self._base( + project={"id": "proj-1", "name": "239633"}, + ) + ) + assert isinstance(item.project, ProjectRef) + assert item.project_id == "proj-1" + assert item.project.name == "239633" + + def test_project_null(self) -> None: + item = ConsumptionItem.model_validate(self._base(project=None)) + assert item.project_id is None + + def test_object_as_object(self) -> None: + item = ConsumptionItem.model_validate( + self._base( + object={ + "id": "obj-1", + "name": "key-1", + "type": "ai_api_key", + "type_name": "Api ключ", + } + ) + ) + assert item.object_id == "obj-1" + assert item.object.type == "ai_api_key" # type: ignore[union-attr] + + def test_object_as_string(self) -> None: + item = ConsumptionItem.model_validate(self._base(object="obj-1")) + assert item.object_id == "obj-1" + + def test_metric_info(self) -> None: + item = ConsumptionItem.model_validate( + self._base( + metric={ + "id": "air_tokens_cost", + "name": "Стоимость", + "unit": "nanorub", + "quantity": "854857248637", + } + ) + ) + assert item.metric is not None + assert item.metric.id == "air_tokens_cost" + assert item.metric.quantity == Decimal("854857248637") + + def test_value_decimal(self) -> None: + item = ConsumptionItem.model_validate(self._base(value=42.5)) + assert item.value == Decimal("42.5") + + +class TestFormatting: + def test_format_rub(self) -> None: + assert format_rub(Decimal("854.83")) == "854,83 ₽" + assert format_rub(0) == "0,00 ₽" + assert format_rub("1234.5") == "1 234,50 ₽" + + def test_format_tokens(self) -> None: + assert format_tokens(0) == "0" + assert format_tokens(1234567) == "1 234 567" + + def test_format_percent(self) -> None: + assert format_percent(99.912) == "99,9%" + assert format_percent(100.0) == "100,0%"