Files
autotest/HISTORY/2026-07-31-session.md
T

448 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-07-31 — Сессия (v1.1.57 → v1.2.1)
## Контекст
Обсуждение архитектуры: унификация ручного и сценарного режимов, гибкие ссылки на инстансы, новый UI редактора сценариев.
## Agent consultations
### Промпт для Opus
Составлен `DOCS/opus-architecture-prompt.md` — полное описание проекта, дублирование CREATE-флоу, ограничение instance_map, вопросы по архитектуре.
### Ответ Opus — 19 уточняющих вопросов
**A. Единый executor (operations/executor.py)**
1. Граница ответственности: executor делает всё до /run, поллинг — снаружи?
2. Обработка ошибок: исключения с типом или dict {ok, error, failed_step}?
3. Трекер: вызывать tracker_add внутри executor для create? Нужны ли сценарные инстансы в трекере?
4. _finish_op: остаётся в api_test.py или выносим в общий модуль поллинга?
**B. Гибкие ссылки на инстансы**
5. Приоритет резолвинга: instance_uid > instance_ref > (для create — новый)
6. Где хранить output→uid: в памяти (instance_map) или в scenario_runs.instance_bindings?
7. Валидация _validate_steps: проверять ссылки на output, уникальность, обязательность
8. Обратная совместимость: поддерживать старый формат (service_id) или мигрировать?
**C. Схема БД**
9. Формат в steps JSONB без изменений схемы? (подтверждено)
10. Использовать существующую колонку scenario_runs.instance_bindings?
**D. UI редактора**
11. Модал vs inline — рекомендация: модал на весь экран
12. Переиспользовать renderParamRow из operations.js в общем модуле?
13. Pre-fill параметров: авто или по кнопке?
14. Формат параметров: символические имена в БД (как сейчас) или иное?
**E. Оптимизация**
15. Вынести _find_uid/_uid_from_location в общий utils
16. Механизм восстановления зависших scenario_runs?
17. Ограничение lock_check: только один RUNNING сценарий — осознанное?
18. _op_results в памяти → мигрировать в БД/Redis?
19. Приоритет: бэкенд или UI редактора в первую очередь?
## Ключевые решения (отвечено DeepSeek)
**A. Executor:**
- A1. Граница: до /run включительно, без поллинга. Возвращает {instance_uid, op_uid, display_name}
- A2. Ошибки: dict {ok, error, failed_step}, не исключения
- A3. Трекер: да, tracker_add внутри executor для всех create
- A4. _finish_op: оставить в api_test.py, цикл поллинга → operations/poll.py
**B. Ссылки:**
- B5. Приоритет: instance_uid > instance_ref > новый create
- B6. Хранение: память + scenario_runs.instance_bindings
- B7. Валидация: проверять ссылки, уникальность, обязательность
- B8. Совместимость: оба формата, без миграции, fallback на service_id
**C. БД:**
- C9. Без изменений схемы
- C10. Использовать instance_bindings
**D. UI:**
- D11. Модал на весь экран
- D12. Общий модуль params-render.js
- D13. Авто pre-fill, все параметры с defaults
- D14. Символические имена в БД
**E. Оптимизация:**
- E15. _find_uid → api/utils.py
- E16. Startup check: TIMEOUT для зависших >1ч
- E17. lock_check оставить
- E18. _op_results не в scope
- E19. Порядок: executor → формат → api_test/scenario → UI
### Финальный план (Опус, утверждён)
Сохранён в `DOCS/opus-plan-2026-07-31.md`. Ветка: `opus-architecture-2026-07-31`.
**4 фазы, 13 шагов:**
**Фаза 1 — Общие модули:**
- `api/utils.py` (NEW) — find_uid(), uid_from_location()
- `operations/poll.py` (NEW) — poll_until_done()
- `operations/executor.py` (NEW) — execute_operation() до /run, без поллинга
**Фаза 2 — Формат шагов:**
- `routes/api_scenario_defs.py` — _validate_steps с output/instance_ref/instance_uid
- `operations/scenario.py` — резолвинг instance_uid > instance_ref > service_id
**Фаза 3 — Миграция вызывающих:**
- `routes/api_test.py` — CMDB delete early return, остальное через executor
- `operations/scenario.py` — через executor + poll_until_done
- `db/init_db.py` — startup cleanup зависших scenario_runs
**Фаза 4 — UI редактора:**
- `static/js/params-render.js` (NEW) — общий рендер параметров
- `static/js/operations.js` — использовать params-render.js
- `static/js/scenario-form.js` — модальный редактор
- `templates/index.html` — разметка модала
### CMDB delete
Жёсткое удаление через `DELETE cmdb-api.deck.nubes.ru/instances/{uid}` (без авторизации).
Нужно для недосозданных инстансов (not created). Остаётся в api_test.py, не в executor.
Добавлено после переписки с Георгием Родионовым 29.07.2026.
## Реализация (v1.2.0 — v1.2.1)
### v1.2.0 — Unified executor + flexible refs
**Новые файлы:** api/utils.py, operations/poll.py, operations/executor.py, static/js/params-render.js
**Изменено:** api_test.py (через executor + poll), scenario.py (output/ref/bindings), api_scenario_defs.py (_validate_steps), init_db.py (cleanup), operations.js (→ params-render), index.html (load order)
- Дублирование CREATE-флоу устранено: ручной и сценарный → один executor
- instance_uid > instance_ref > service_id (гибкие ссылки)
- Общий поллинг poll_until_done()
- +254 / −277 строк (меньше кода)
### v1.2.1 — Opus review fixes
**Критическое:** tracker_add внутрь executor (защита от сирот, A3)
**Исправлено:** labelCls в renderMapFixedRow, descr с контекстом, порядок скриптов
**5 файлов:** executor.py, api_test.py, scenario.py, params-render.js, app.py
### Проверка в поде (v1.2.1)
- Все 4 новых файла на месте
- 11 JS → 200, порядок правильный
- Schema OK, seed OK, API отвечает
## Что дальше
**Фаза 4 — модальный редактор сценариев (✅ v1.2.2-v1.2.4):**
- ✅ Модал с дропдаунами сервисов/операций
- ✅ output/instance_ref с облачными инстансами
- ✅ Кнопки CRUD крупнее, справка «📖 Как заполнять»
### v1.2.16 — instance_meta JSONB
После каждого прогона сохраняется полная информация об инстансе (GET /instances/{uid}).
Все поля кроме instanceUid/displayName/svc/serviceId/explainedStatus.
## Идеи на будущее (НЕ ДЕЛАТЬ, обдумать)
**Context snapshot:** сохранять снапшот ВСЕХ инстансов пользователя на момент запуска
(instanceUid, displayName, serviceId, svc, explainedStatus, specification).
При анализе FAIL — видеть контекст: «было 3 running Болванки, возможно конфликт ресурсов».
Хранить в `runs.context_snapshot JSONB` и `scenario_runs.context_snapshot JSONB`.
Данные обезличенные, не гигабайты. Отложено до реальной необходимости.
---
## Аудит безопасности GPT-5.3-Codex (2026-07-31)
Проведён полный code review 29 файлов (~6000 строк). Найдено 11 проблем.
Результаты зафиксированы в DOCS/ARCHITECTURE.md (раздел 8).
### КРИТИЧЕСКИЕ (исправлены)
1. **XSS через params в scenario-list.js:119** — k/v параметров в innerHTML без `_esc`.
Stored XSS через БД сценариев. → v1.2.19
2. **JS injection в onclick** (scenario-list.js:126-128) — `def.name` в `'...'` без JS-escape.
`_esc` не экранирует `'` → разрыв строки. → v1.2.19
3. **Гонка `_op_results`** (api_test.py:264-280) — dict без lock, читается/пишется/чистится
из нескольких потоков. → v1.2.20: `threading.Lock()` + `pop(k, None)`
4. **Неатомарный lock сценариев** (scenario_defs.py + api_scenario_run.py) —
`lock_check` (SELECT) и `INSERT RUNNING` разделены. → v1.2.20: `pg_try_advisory_lock`
### СРЕДНИЕ (исправлены)
5. **Lost update трекера** (tracker.py) — `_locked_read` + `_locked_write` в разных lock.
→ v1.2.19: `_atomic_update()` под одним lock
6. **Зависание UI поллинга** (scenario-list.js:211) — пустой catch, `busy` не сбрасывается.
→ v1.2.19: счётчик ошибок + `stopScenarioPoll` + `busy=false`
7. **`has_target` не проверяется** (api_scenario_defs.py:58) — вычисляется и игнорируется.
→ v1.2.19: явная проверка
8. **`_ensure_schema` silent** (pool.py:64) — `except Exception: pass`.
→ v1.2.20: `traceback.print_exc()`
### ПОТЕНЦИАЛЬНЫЕ (исправлены)
9. **Stale async в редакторе** (scenario-form.js) — `loadStepParams` после `renderEditor`
может перезаписать новый DOM. → v1.2.20: `_renderGen` generation token
10. **validate-cfs хрупкий** (terraform.py:250-254) — фильтрация по тексту исключения.
→ v1.2.20: явный `except json.JSONDecodeError`
### НЕ ИСПРАВЛЕНО (архитектурное ограничение)
11. **In-memory `_op_results` на воркер** — не shared между gunicorn-воркерами.
Статус иногда читается из API fallback. Решение: Redis/БД для статусов.
Отложено — низкая вероятность проблемы на практике (2 воркера, stickiness).
---
## Повторный аудит Codex (2026-07-31, вторая итерация)
Codex проверил исправления и нашёл **критические ошибки в моих же фиксах**:
### ОШИБКА AI #1: Advisory lock сломан (v1.2.20)
**Что я сделал:** `lock_check()` брал `pg_try_advisory_lock` на соединении `conn1`,
возвращал `True`, и `conn1` уходил обратно в пул. `unlock_scenario()` вызывал
`get_conn()` → получал `conn2` (другое соединение!) → unlock на `conn2` не снимал
lock с `conn1`. Плюс ранние `return` в `api_scenario_run.py` после успешного
`lock_check` вообще не вызывали unlock.
**Почему ошибся:** не учёл что PostgreSQL advisory lock привязан к сессии (соединению),
а соединения возвращаются в пул. Передача соединения между `lock_check` и потоком
сценария требовала бы сложной оркестрации.
**Как исправлено (v1.2.21):** заменён на **partial unique index** на уровне БД:
```sql
CREATE UNIQUE INDEX idx_one_running
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING';
```
Теперь `INSERT INTO scenario_runs ... status='RUNNING'` сам становится атомарной
проверкой — вторая вставка получает unique violation → 409 без гонок.
`lock_check` возвращён к простому SELECT (быстрая предпроверка для красивого 409).
`unlock_scenario` удалён полностью. `try/finally` из `run_scenario` убран.
### ОШИБКА AI #2: escName не экранирует `"` (v1.2.19)
**Что я сделал:** `def.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'")`
экранировал `\` и `'` для JS-строки, но забыл `"` для HTML-атрибута `onclick="..."`.
**Почему ошибся:** фокусировался только на JS-контексте (строка в `'...'`),
не учёл что она внутри HTML-атрибута в `"..."`.
**Как исправлено (v1.2.21):** добавлено `.replace(/"/g,'"')` в `escName`.
### НОВАЯ находка Codex: instances.js:78
`instanceUid` в `onclick="toggleInstance('${i.instanceUid}')"` — теоретически уязвим,
но на практике UUID всегда `[a-f0-9-]+` → безопасен. Отмечен как низкий риск,
исправление не требуется.
### ИТОГ
| # | Слой | Статус |
|---|------|--------|
| Advisory lock | Python | ❌ СЛОМАН → ✅ partial unique index |
| escName `"` | JS | ❌ Неполный → ✅ добавлен `"` |
| instances.js onclick | JS | ⚠️ Низкий риск, UUID безопасен |
| `lock_check` fallback | Python | ✅ `True``False` при ошибке БД |
---
## Третий аудит Codex + трёхсостояночный lock_check (v1.2.22-v1.2.23)
Codex проверил v1.2.21 и нашёл 3 проблемы. Две исправлены, одну — обсудили и
пришли к правильному решению:
### Исправлено
1. **UniqueViolation → 500 (не 409)**`api_scenario_run.py:78`.
INSERT ловился общим `except` → 500. Теперь: `e.pgcode == '23505'` → 409. (v1.2.22)
2. **escName без `&`**`scenario-list.js:128`.
`'` декодируется браузером в `'` до JS → разрыв строки.
Добавлен `.replace(/&/g,'&')` ПЕРВЫМ шагом. (v1.2.22)
### Обсуждено и исправлено правильно
3. **`lock_check` fallback — трёхсостояночный подход** (v1.2.23):
Исходно Codex предложил `True``False` при no-db. AI слепо сделал.
Пользователь возразил: `False` ломает запуск при деградации БД.
Codex согласился и предложил трёхсостояночный возврат:
- `True` — можно запускать (нет RUNNING)
- `False` — нельзя (есть RUNNING) → 409
- `None` — БД недоступна → 503
`api_scenario_run.py` обрабатывает `None` как 503 DB unavailable.
### Созданы тесты (Codex, только сохранены, не запущены)
`tests/` — 4 файла, покрывают критические фиксы:
| Файл | Что тестирует |
|------|---------------|
| `conftest.py` | Flask test client + sys.path |
| `test_api_scenario_run.py` | 503 при `lock_check=None`, 409 при `False`, 409 при `UniqueViolation(pgcode=23505)` |
| `test_db_scenario_defs.py` | `lock_check → None` при no-db и ошибке БД |
| `test_static_regressions.py` | Статическая проверка `idx_one_running` в init_db.py и цепочки `escName` в scenario-list.js |
---
## План: эмулятор Nubes API для интеграционных тестов
### Зачем
Реальные тесты медленные (поллинг до 30 минут) и жрут ресурсы облака.
Эмулятор даст: быстрые тесты (< 1 сек), детерминизм, краевые случаи, CI/CD.
### Архитектура
```
site/
├── app.py # основное приложение
└── mock/
└── nubes_mock.py # эмулятор API Nubes (Flask, порт 5001)
```
В `app.py` — переключение по `NUBES_MOCK=1` → эндпоинт `http://localhost:5001/api/v1/svc`.
### Эндпоинты для эмуляции (Болванка, сервис 1)
| Метод | Путь | Ответ |
|-------|------|-------|
| POST | `/instances` | 201 + `{instanceUid}` |
| GET | `/instances?pageSize=200` | `{results: [...]}` |
| GET | `/instances/{uid}` | `{instance: {state: {params: {...}}}}` |
| GET | `/services` | `{results: [{svcId: 1, svc: "dummy"}]}` |
| GET | `/services/1` | `{svc: {operations: [...]}}` |
| POST | `/instanceOperations` | `{instanceOperationUid}` |
| POST | `/instanceOperationCfsParams` | `{}` |
| POST | `/instanceOperations/{uid}/run` | `{}` |
| GET | `/instanceOperations/{uid}?fields=...` | `{instanceOperation: {dtFinish, isSuccessful, ...}}` |
| GET | `/instanceOperations/default/{id}` | `{svcOperation: {cfsParams: [...]}}` |
| GET | `/instanceOperations/{uid}/validate-cfs` | `{}` (200 OK) |
### Что сложнее
- `cfsParams` — у каждого сервиса своя структура, придётся хардкодить под Болванку
- `refSvcId` — ссылки на другие сервисы (External IP и т.д.) — отложить
- `stages` — этапы выполнения (plan→apply→...) — отдавать фейковые
### Порядок создания
1. `mock/nubes_mock.py` — Flask-заглушка (~150 строк)
2. Интеграционный тест: сценарий `create→modify→delete` через `app_client`
3. `NUBES_MOCK=1` в `app.py` для переключения эндпоинта
---
## Архитектура мок-полигона (Опус, 2026-07-31)
### Принятые решения (10/10)
| Q | Решение | Обоснование |
|---|---------|-------------|
| Q1 | Отдельный процесс :5001 (A) | `http_client` делает реальные GET/POST — blueprint не проверит |
| Q2 | Папка `polygon/services/*.yaml` | Каждый сервис в своём файле |
| Q3 | Мин. поля (id+код+тип), остальное достраивается | 20+ параметров вручную — ад |
| Q4 | Ленивый dtFinish (A) | Без потоков, детерминированно, `MOCK_OP_DELAY=0.1` |
| Q5 | Единая стейт-машина | create→running→suspended→deleted |
| Q6 | Реальный мерж params | Иначе тест modify→проверить state.params бессмысленен |
| Q7 | Статический stateOut из YAML | Для MVP, генерация потом |
| Q8 | `/_mock/reset` | Без сброса тесты влияют друг на друга |
| Q9 | Тесты через `app_client` | Проверяет реальную связку, не только эмулятор |
| Q10 | refSvcId игнорируем в MVP | validate-cfs всегда OK |
### Критические точки интеграции (из кода)
1. **Location обязателен.** `HttpClient.post` достаёт UUID из заголовка `Location`.
Мок ОБЯЗАН отдавать `Location: ./<uuid>` на POST /instances и POST /instanceOperations.
2. **Поллинг спит 5с.** `poll_until_done` делает GET, затем `time.sleep(5)`.
При `MOCK_OP_DELAY=0` dtFinish появится на первом же GET — без задержки.
3. **Точка переключения — auth.py.** `get_client()``detect_endpoint()`.
Нужен short-circuit: при `NUBES_MOCK=1` возвращать `http://localhost:5001/api/v1/svc`
и НЕ вызывать `detect_endpoint`.
4. **validate-cfs = пустое тело.** Мок отдаёт 200 с пустым телом.
5. **state.params по коду, cfsParams по числовому id.** YAML должен связывать
`svcOperationCfsParamId` ↔ код параметра.
### План реализации (3 фазы, 12 шагов)
**Фаза 1 — MVP (create + поллинг):**
1. `polygon/defaults.py``default_for(dataType)` по типу
2. `polygon/config_loader.py` — загрузка `services/*.yaml`, достройка defaults
3. `polygon/state.py``MockState`: instances, operations, create, run, ленивый dtFinish, reset
4. `polygon/server.py` — Flask, префикс `/api/v1/svc`, Location-заголовки
5. `polygon/services/dummy.yaml` — Болванка (id параметров из HAR)
6. Интеграция в `auth.py`: short-circuit по `NUBES_MOCK`
**Фаза 2 — полный CRUD + сервисы:**
7. GET /instances/{uid} (state.params/state.out), GET /instances (пагинация)
8. GET /instanceOperations/default/{id}, GET /services, GET /services/{id}
9. `apply_effect`: modify→мерж params, delete→удаление, suspend/resume→статус
10. `/_mock/reset` + `polygon/services/postgresql.yaml`
**Фаза 3 — тесты:**
11. `tests/conftest.py` — фикстура поднятия мока + автосброс
12. `tests/test_mock_integration.py` — 5 сценариев через `app_client`
### Структура файлов
```
app-autotest/
├── site/
│ ├── api/auth.py # +short-circuit localhost (НЕ NUBES_MOCK)
│ └── ...
└── polygon/
├── server.py # Flask-приложение эмулятора
├── state.py # MockState (в памяти)
├── config_loader.py # загрузка YAML + достройка defaults
├── defaults.py # default_for(dataType)
└── services/
├── dummy.yaml # Болванка
└── postgresql.yaml # PostgreSQL (stateOut: users, databases)
```
### Short-circuit: localhost вместо NUBES_MOCK
Опус ПОДТВЕРДИЛ что проверка `localhost` лучше отдельного флага:
- `detect_endpoint()` хардкодит dev/test и не читает `NUBES_API_ENDPOINT` — всегда лезет в реальные стенды
- При `NUBES_API_ENDPOINT=http://localhost:5001` detect может «угадать» реальный стенд и увести мимо мока
- Решение: в `get_client()` и `get_stand()` — если endpoint начинается с `http://localhost` или `http://127.0.0.1` → пропустить detect, сразу использовать endpoint
- `stand_name("http://localhost:5001")``"?"` — поэтому `get_stand()` при localhost возвращает `"mock"`
- Ноль новых env-переменных, существующий `NUBES_API_ENDPOINT` уже в конфиге
### Реальные ID из HAR для dummy.yaml
Опус распарсил `development/dummycreate.har` и `development/dummymodify.har`:
**Операции Болванки (serviceId=1):**
| operation | svcOperationId |
|-----------|----------------|
| create | 18 |
| delete | 71 |
| modify | 92 |
| suspend | 93 |
| resume | 94 |
| redeploy | 240 |
**Параметры create (svcOperationCfsParamId):**
| id | код | тип |
|----|-----|-----|
| 242 | resourceRealm | string (valueList=["dummy"]) |
| 198 | durationMs | integer |
| 199 | *(код не извлечён)* | boolean |
| 200 | *—* | boolean |
| 201 | *—* | integer/enum |
| 286 | *—* | string |
| 321 | *—* | map + dataDescriptor |
| 322 | *—* | string |
| 396 | *—* | string |
| 647 | *—* | map |
| 654 | *—* | array(map) |
| 863 | *—* | array |
Для мока коды некритичны (мок сам отдаёт и шаблон и state.params — они должны совпадать между собой). Для реалистичности — код в `mock-architecture-prompt.md` достанет полные коды одной строкой Python.