diff --git a/DOCS/test-results-history-plan.md b/DOCS/test-results-history-plan.md new file mode 100644 index 0000000..649a0cd --- /dev/null +++ b/DOCS/test-results-history-plan.md @@ -0,0 +1,260 @@ +# История тестов и просмотр результатов — план реализации + +> Написано 2026-07-26 для ревью Соннетом перед реализацией. + +## Контекст + +Приложение `app-autotest` (Flask, `site/app.py`) — автотесты операций облачных сервисов Nubes. +Работает с dummy-сервисом (svcId=1, Болванка). Уже реализовано: + +- UI: инфраструктура, сервисы, инстансы, кнопки операций, стадии в реальном времени +- Бэкенд: полный поток CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY 1:1 с Terraform-провайдером +- Трекер: `site/operations/tracker.py` — `/tmp/instances.json` (только инстансы созданные приложением) +- Поллинг: `/api/test` возвращает `opUid` сразу, `/api/test/status/` — стадии + +## Проблема + +В deck-test (UI облака) для каждого инстанса хранится **полная история операций**: +список всех modify/suspend/delete/... с датами, статусами, этапами, параметрами. Можно открыть любую операцию и посмотреть как выполнялась. + +У нас история **не сохраняется**. После завершения операции результат теряется. Нельзя: +- Посмотреть что и когда запускали +- Сравнить два запуска +- Понять какая операция упала и почему + +## Цель + +Сделать аналог истории deck-test, **но лучше** — потому что наше приложение не просто UI для сервиса, а **инструмент тестирования**. + +## Что есть в deck-test (базовый минимум) + +| Фича | Описание | +|------|---------| +| Список операций инстанса | Таблица: UUID, название, дата, код ответа, начало, окончание | +| Детали операции | Параметры, этапы (stages), статус, длительность, ошибка | +| Фильтр по типу | Показать только modify / только suspend / ... | + +## Что МОЖЕМ сделать лучше (тест-ориентированные фичи) + +| Фича | Зачем | +|------|-------| +| **Сводная таблица всех тестов** (кросс-инстанс) | Не прыгать между инстансами — видеть всё в одном месте | +| **Статистика:** % успешных, среднее время по операции | Быстро понять что ломается чаще всего | +| **Сравнение двух запусков** | «После modify время выросло на 2 секунды» | +| **Экспорт в JSON** | Забрать результаты в CI/CD пайплайн | +| **Повторный запуск** из истории (те же параметры) | Не заполнять форму заново | +| **Таймлайн** с цветовой кодировкой | Визуально видеть последовательность: create → modify → suspend → resume → delete | + +## Архитектура хранения + +Сейчас: `instances.json` (JSON-файл в `/tmp/`). Для истории нужна БД. + +### Варианты + +1. **SQLite** в `/tmp/autotest_results.db` — ноль зависимостей, встроен в Python +2. JSON-файл `/tmp/results.json` — проще, но неудобно для запросов/фильтрации + +**Выбран SQLite** — фильтрация, сортировка, аггрегация (статистика). + +### Схема БД + +```sql +CREATE TABLE test_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + instance_uid TEXT NOT NULL, + instance_name TEXT NOT NULL, + operation TEXT NOT NULL, -- create/modify/suspend/delete/resume/redeploy + svc_operation_id INTEGER NOT NULL, + op_uid TEXT NOT NULL, -- UUID операции в облаке + status TEXT NOT NULL, -- OK/FAIL/TIMEOUT + error TEXT DEFAULT '', + duration_ms REAL DEFAULT 0, -- миллисекунды + started_at TEXT NOT NULL, -- ISO datetime + finished_at TEXT DEFAULT '' +); + +CREATE TABLE test_stages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + test_run_id INTEGER NOT NULL REFERENCES test_runs(id), + stage_name TEXT NOT NULL, -- "1. Валидация" + is_successful INTEGER, -- 0/1/NULL + duration_sec REAL DEFAULT 0, + stage_msg TEXT DEFAULT '', + sort_order INTEGER DEFAULT 0 +); + +CREATE TABLE test_params ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + test_run_id INTEGER NOT NULL REFERENCES test_runs(id), + param_name TEXT NOT NULL, + param_value TEXT NOT NULL +); +``` + +### Почему SQLite а не `/tmp/*.json` + +- `/tmp/` на поде **не затирается** при clone (в отличие от `site/`) +- SQLite даёт `WHERE`, `ORDER BY`, `GROUP BY` из коробки +- Файл БД можно скачать для офлайн-анализа +- Нет дополнительных зависимостей (sqlite3 встроен в Python) + +## API эндпоинты + +### `GET /api/results` + +Параметры: +- `instance_uid` — фильтр по инстансу +- `operation` — фильтр по типу +- `status` — OK/FAIL/TIMEOUT +- `limit` — сколько (по умолчанию 50) +- `offset` — смещение + +Ответ: +```json +{ + "total": 42, + "results": [ + { + "id": 1, + "instance_uid": "884356cf-...", + "instance_name": "autotest-1-first", + "operation": "modify", + "status": "OK", + "duration_ms": 4200, + "started_at": "2026-07-26T14:12:18", + "stages_count": 5, + "stages_failed": 0 + } + ] +} +``` + +### `GET /api/results/` + +Полные детали: все стадии + все параметры. + +### `DELETE /api/results/` + +Удалить запись. + +### `GET /api/results/stats` + +Аггрегация по операциям: +```json +{ + "modify": {"total": 10, "ok": 9, "fail": 1, "avg_duration_ms": 5200}, + "suspend": {"total": 8, "ok": 8, "fail": 0, "avg_duration_ms": 3400} +} +``` + +## Интеграция с текущим кодом + +### `site/operations/results_db.py` (новый) + +```python +import sqlite3 +import os + +DB_PATH = "/tmp/autotest_results.db" + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + _ensure_tables(conn) + return conn + +def save_result(instance_uid, instance_name, operation, svc_op_id, op_uid, status, error, duration_ms, stages, params): + ... + +def get_results(instance_uid=None, operation=None, status=None, limit=50, offset=0): + ... + +def get_result(id): + ... + +def delete_result(id): + ... + +def get_stats(): + ... +``` + +### Изменения в `site/routes/api_test.py` + +В `_finish_op()` — после завершения операции: +```python +from operations.results_db import save_result +save_result( + instance_uid=instance_uid, + instance_name=display_name, + operation="modify", + svc_op_id=svc_op_id, + op_uid=op_uid, + status=status, + error=error, + duration_ms=duration_ms, + stages=stages, + params=params +) +``` + +Добавить 4 новых роута для `/api/results*`. + +## UI + +### Новая карточка «История тестов» + +Под карточкой «Инстансы»: + +``` +┌─────────────────────────────────────────┐ +│ История тестов │ +│ [Все▼] [create] [modify] [suspend] ... │ ← фильтры-кнопки +│ │ +│ # Инстанс Операция Рез-т │ +│ 1 autotest-1-first modify ✅ 4.2s │ ← кликабельно +│ 2 curl-test-144834 suspend ✅ 3.1s │ +│ 3 autotest-1-first delete ❌ err │ +│ │ +│ Статистика: modify 9/10 OK (90%) ... │ +└─────────────────────────────────────────┘ +``` + +Клик по строке → раскрываются детали (стадии + параметры). + +### Цветовая кодировка + +- ✅ зелёный — OK +- ❌ красный — FAIL +- ⏱ жёлтый — TIMEOUT + +## План реализации (фазы) + +### Фаза 1: база + сохранение (MVP) + +1. `site/operations/results_db.py` — модуль SQLite +2. `_finish_op()` — сохранение результата +3. `GET /api/results` — базовый список +4. UI — простая таблица без фильтров + +### Фаза 2: детали + фильтры + +1. `GET /api/results/` — детали +2. `GET /api/results/stats` — статистика +3. UI — фильтры, раскрытие деталей, статистика + +### Фаза 3: продвинутые фичи + +1. Экспорт JSON +2. Повторный запуск из истории +3. Таймлайн +4. Сравнение запусков + +## Вопросы к Соннету + +1. SQLite в `/tmp/` на поде — насколько это надёжно для тестовой истории? Может лучше PostgreSQL-сервис Nubes? +2. Схема БД — нормально или что-то упущено? +3. Стоит ли хранить `stageMsg` (большие JSON-строки с логами Jenkins)? +4. API эндпоинты — достаточный набор или добавить что-то? +5. UI — есть ли смысл делать SPA-подобный роутинг или оставить серверный рендеринг как сейчас? +6. Экспорт результатов — JSON потоком или файлом?