Files
2026-09-02 16:47:27 +03:00

297 lines
9.9 KiB
Python

"""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"