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

24 KiB
Raw Blame History

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

СРЕДНИЕ (исправлены)

  1. Lost update трекера (tracker.py) — _locked_read + _locked_write в разных lock. → v1.2.19: _atomic_update() под одним lock

  2. Зависание UI поллинга (scenario-list.js:211) — пустой catch, busy не сбрасывается. → v1.2.19: счётчик ошибок + stopScenarioPoll + busy=false

  3. has_target не проверяется (api_scenario_defs.py:58) — вычисляется и игнорируется. → v1.2.19: явная проверка

  4. _ensure_schema silent (pool.py:64) — except Exception: pass. → v1.2.20: traceback.print_exc()

ПОТЕНЦИАЛЬНЫЕ (исправлены)

  1. Stale async в редакторе (scenario-form.js) — loadStepParams после renderEditor может перезаписать новый DOM. → v1.2.20: _renderGen generation token

  2. validate-cfs хрупкий (terraform.py:250-254) — фильтрация по тексту исключения. → v1.2.20: явный except json.JSONDecodeError

НЕ ИСПРАВЛЕНО (архитектурное ограничение)

  1. 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 на уровне БД:

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 TrueFalse при ошибке БД

Третий аудит 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)

Обсуждено и исправлено правильно

  1. lock_check fallback — трёхсостояночный подход (v1.2.23):

    Исходно Codex предложил TrueFalse при 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.pydefault_for(dataType) по типу
  2. polygon/config_loader.py — загрузка services/*.yaml, достройка defaults
  3. polygon/state.pyMockState: 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.