Add tests/test_tui.py
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
"""Headless-тесты TUI (Textual pilot)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from decimal import Decimal
|
||||
|
||||
from textual.widgets import DataTable, Input, Static
|
||||
|
||||
from selectel_ml_tui.config import Credentials, Settings
|
||||
from selectel_ml_tui.models import (
|
||||
ApiKeyUsage,
|
||||
BudgetInfo,
|
||||
Consumption,
|
||||
HistoryPoint,
|
||||
Metrics,
|
||||
ModelMetrics,
|
||||
TokenUsage,
|
||||
)
|
||||
from selectel_ml_tui.services import DashboardData
|
||||
from selectel_ml_tui.storage import Storage
|
||||
from selectel_ml_tui.ui.app import SelectelMLTUI
|
||||
|
||||
|
||||
class FakeService:
|
||||
"""Сервис-заглушка: возвращает готовые данные."""
|
||||
|
||||
def __init__(self, data: DashboardData) -> None:
|
||||
self.data = data
|
||||
self.refresh_count = 0
|
||||
|
||||
async def refresh(self) -> DashboardData:
|
||||
self.refresh_count += 1
|
||||
return self.data
|
||||
|
||||
|
||||
def _dashboard_data() -> DashboardData:
|
||||
return DashboardData(
|
||||
month_consumption=Consumption.model_validate(
|
||||
{
|
||||
"period": "2026-08-01T00:00:00",
|
||||
"amount_rub": "854.83",
|
||||
"requests": 1034,
|
||||
"tokens_total": 451607518,
|
||||
"input_tokens": 100,
|
||||
"output_tokens": 200,
|
||||
"cache_read_tokens": 30,
|
||||
"cache_write_tokens": 40,
|
||||
}
|
||||
),
|
||||
metrics=Metrics.model_validate(
|
||||
{
|
||||
"availability_pct": 99.9,
|
||||
"avg_ttft_ms": 0.4,
|
||||
"avg_latency_ms": 1.2,
|
||||
"requests": 1035,
|
||||
"input_tokens": 5,
|
||||
}
|
||||
),
|
||||
models=[
|
||||
ModelMetrics.model_validate(
|
||||
{
|
||||
"model_id": "gpt-4o",
|
||||
"model_name": "GPT-4o",
|
||||
"requests": 69,
|
||||
"availability_pct": 100.0,
|
||||
"avg_ttft_ms": 3.35,
|
||||
}
|
||||
)
|
||||
],
|
||||
keys=[
|
||||
ApiKeyUsage(
|
||||
name="Mei",
|
||||
prefix="sk-sl-v1-9dd61ec",
|
||||
is_active=True,
|
||||
spend_limit_rub=Decimal("1300"),
|
||||
spend_rub=Decimal("1082.95"),
|
||||
remaining_rub=Decimal("217.05"),
|
||||
)
|
||||
],
|
||||
budget=BudgetInfo(
|
||||
spend_limit_rub=Decimal("2000"),
|
||||
spend_rub=Decimal("1077.87"),
|
||||
remaining_rub=Decimal("922.13"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_app(tmp_path, data: DashboardData | None = None) -> SelectelMLTUI:
|
||||
settings = Settings(refresh_interval=3600)
|
||||
creds = Credentials(static_token="tok")
|
||||
storage = Storage(tmp_path / "cache.db")
|
||||
service = FakeService(data or _dashboard_data())
|
||||
return SelectelMLTUI(
|
||||
settings=settings,
|
||||
credentials=creds,
|
||||
storage=storage,
|
||||
service=service,
|
||||
)
|
||||
|
||||
|
||||
async def test_dashboard_renders_data(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
month = app.screen.query_one("#month-panel", Static)
|
||||
rendered = str(month.render())
|
||||
assert "854,83" in rendered
|
||||
assert "Потребление за месяц" in rendered
|
||||
metrics = app.screen.query_one("#metrics-panel", Static)
|
||||
assert "99,9%" in str(metrics.render())
|
||||
table = app.screen.query_one("#models-table", DataTable)
|
||||
assert table.row_count == 1
|
||||
assert "GPT-4o" in table.get_row_at(0)
|
||||
|
||||
|
||||
async def test_refresh_hotkey_reruns_service(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
service = app._service
|
||||
assert service.refresh_count == 1
|
||||
await pilot.press("r")
|
||||
await pilot.pause()
|
||||
assert service.refresh_count == 2
|
||||
|
||||
|
||||
async def test_navigate_to_history_and_settings(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# history
|
||||
await pilot.press("h")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "HistoryScreen"
|
||||
# назад
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
# settings
|
||||
await pilot.press("s")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "SettingsScreen"
|
||||
info = app.screen.query_one("#settings-info", Static).render()
|
||||
assert "API base URL" in str(info)
|
||||
assert "tok" in str(info)
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
|
||||
|
||||
async def test_history_screen_shows_points(tmp_path) -> None:
|
||||
storage = Storage(tmp_path / "cache.db")
|
||||
storage.save_history(
|
||||
HistoryPoint(
|
||||
timestamp=dt.datetime(2026, 8, 18, 0, 0),
|
||||
amount_rub=Decimal("854.83"),
|
||||
tokens=TokenUsage(input_tokens=100),
|
||||
)
|
||||
)
|
||||
app = _make_app(tmp_path)
|
||||
app._storage = storage
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("h")
|
||||
await pilot.pause()
|
||||
table = app.screen.query_one("#history-table", DataTable)
|
||||
assert table.row_count == 1
|
||||
cells = [str(cell) for cell in table.get_row_at(0)]
|
||||
assert any("854,83" in cell for cell in cells)
|
||||
bars = app.screen.query_one("#history-bars")
|
||||
assert bars.render() != "Нет данных для графика"
|
||||
|
||||
|
||||
async def test_history_period_switch(tmp_path) -> None:
|
||||
storage = Storage(tmp_path / "cache.db")
|
||||
for day in range(20):
|
||||
storage.save_history(
|
||||
HistoryPoint(
|
||||
timestamp=dt.datetime(2026, 8, day + 1, 0, 0),
|
||||
amount_rub=Decimal(str(day + 1)),
|
||||
tokens=TokenUsage(input_tokens=day + 1),
|
||||
)
|
||||
)
|
||||
app = _make_app(tmp_path)
|
||||
app._storage = storage
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("h")
|
||||
await pilot.pause()
|
||||
title = app.screen.query_one("#history-title", Static)
|
||||
assert "30 дней" in str(title.render())
|
||||
table = app.screen.query_one("#history-table", DataTable)
|
||||
rows_30 = table.row_count
|
||||
await pilot.click("#period-7")
|
||||
await pilot.pause()
|
||||
title = app.screen.query_one("#history-title", Static)
|
||||
assert "7 дней" in str(title.render())
|
||||
table = app.screen.query_one("#history-table", DataTable)
|
||||
assert table.row_count < rows_30
|
||||
await pilot.click("#period-90")
|
||||
await pilot.pause()
|
||||
assert "90 дней" in str(app.screen.query_one("#history-title", Static).render())
|
||||
assert app.screen.query_one("#history-table", DataTable).row_count >= rows_30
|
||||
|
||||
|
||||
async def test_model_detail_on_enter(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "ModelDetailScreen"
|
||||
detail = app.screen.query_one("#model-detail", Static)
|
||||
rendered = str(detail.render())
|
||||
assert "GPT-4o" in rendered
|
||||
assert "Запросы" in rendered
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
|
||||
|
||||
async def test_settings_save_interval(tmp_path, monkeypatch) -> None:
|
||||
saved = {}
|
||||
|
||||
def fake_save(settings) -> None:
|
||||
saved["interval"] = settings.refresh_interval
|
||||
|
||||
monkeypatch.setattr("selectel_ml_tui.ui.app.save_settings", fake_save)
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("s")
|
||||
await pilot.pause()
|
||||
inp = app.screen.query_one("#interval-input", Input)
|
||||
inp.value = "120"
|
||||
await pilot.click("#save-settings")
|
||||
await pilot.pause()
|
||||
assert app._settings.refresh_interval == 120
|
||||
assert saved["interval"] == 120
|
||||
|
||||
|
||||
async def test_settings_clear_cache(tmp_path) -> None:
|
||||
storage = Storage(tmp_path / "cache.db")
|
||||
storage.save_history(
|
||||
HistoryPoint(
|
||||
timestamp=dt.datetime(2026, 8, 18, 0, 0),
|
||||
amount_rub=Decimal("854.83"),
|
||||
tokens=TokenUsage(input_tokens=100),
|
||||
)
|
||||
)
|
||||
app = _make_app(tmp_path)
|
||||
app._storage = storage
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("s")
|
||||
await pilot.pause()
|
||||
await pilot.click("#clear-cache")
|
||||
await pilot.pause()
|
||||
assert storage.get_history(days=30) == []
|
||||
|
||||
|
||||
async def test_offline_status_in_subtitle(tmp_path) -> None:
|
||||
data = _dashboard_data()
|
||||
data.offline = True
|
||||
app = _make_app(tmp_path, data)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert "offline" in app.sub_title
|
||||
|
||||
|
||||
async def test_window_resize(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test(size=(120, 40)) as pilot:
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
await pilot.resize_terminal(80, 24)
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
month = app.screen.query_one("#month-panel", Static)
|
||||
assert "854,83" in str(month.render())
|
||||
|
||||
|
||||
async def test_keys_screen(tmp_path) -> None:
|
||||
app = _make_app(tmp_path)
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
await pilot.press("k")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "KeysScreen"
|
||||
table = app.screen.query_one("#keys-table", DataTable)
|
||||
assert table.row_count == 1
|
||||
cells = [str(cell) for cell in table.get_row_at(0)]
|
||||
assert any("Mei" in cell for cell in cells)
|
||||
assert any("1 300" in cell for cell in cells)
|
||||
budget = app.screen.query_one("#keys-budget", Static)
|
||||
assert "Бюджет роутера" in str(budget.render())
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert app.screen.__class__.__name__ == "DashboardScreen"
|
||||
Reference in New Issue
Block a user