# Opus Implementation Plan — Унификация сценариев autotest (2026-07-31) > Основано на DOCS/opus-questions-2026-07-31.md (19 Q+A) и ревью пользователя. ## Цель Устранить дублирование CREATE-флоу (ручной api_test.py vs сценарный scenario.py), ввести единый execute_operation, гибкие ссылки на инстансы (output/instance_ref/instance_uid), модальный редактор сценариев с богатым рендером параметров. ## Порядок реализации (E19): бэкенд → формат → миграция вызовов → UI --- ## Фаза 1 — Общие модули (фундамент) ### 1. api/utils.py (NEW) - `find_uid(resp)` — поиск UUID по ключам: instanceOperationUid → instanceUid → uid. - `uid_from_location(loc)` — UUID из Location-заголовка. - Заменяет 2 дубля (в api_test.py и scenario.py — сейчас разные реализации). ### 2. operations/poll.py (NEW) - `poll_until_done(client, op_uid, timeout=1800)` → dict {status, is_successful, error_log, stages, duration, svc}. - Критерий завершения: dtFinish != "" (НЕ isInProgress). - Общий цикл для async (_finish_op) и sync (run_scenario). Убирает хардкод 1800s в 3 местах. ### 3. operations/executor.py (NEW) - `execute_operation(client, service_id, operation, instance_uid, params, svc_op_id=None, display_name=None)` → dict {ok, error, failed_step, instance_uid, op_uid, display_name}. - Делает всё ДО /run включительно: POST /instances (только create) → POST /instanceOperations → send_params_terraform → POST /run. НЕ поллит. - Ветвится по operation=="create" ВНУТРИ: - create: POST /instances → op_payload {instanceUid, operation}; svc_op_id игнорируется. - non-create: op_payload {instanceUid, svcOperationId, operation}. - failed_step ∈ {instances, instanceOperations, params, run}. - tracker_add вызывается ВНУТРИ executor для всех create (E3) — сразу после получения instanceUid, до params/run (защита от сирот). - Переиспользует существующую send_params_terraform (operations/terraform.py) как есть. --- ## Фаза 2 — Формат шагов и резолвинг (depends Фаза 1) ### 4. routes/api_scenario_defs.py — _validate_steps Новые опциональные ключи шага: output, instance_ref, instance_uid. Валидация: - output уникален в пределах сценария. - instance_ref ссылается на output из ПРЕДЫДУЩИХ шагов. - для не-create шага обязателен instance_ref | instance_uid | (fallback старый формат). - Старый формат (только service_id) продолжает проходить валидацию. ### 5. operations/scenario.py — резолвинг инстанса Приоритет: instance_uid > instance_ref > instance_map[service_id] (fallback). После create: bindings[output] = instance_uid — в память И в scenario_runs.instance_bindings (колонка JSONB уже есть, DEFAULT '{}'). Резолвинг во время выполнения — ТОЛЬКО из памяти (БД для наблюдаемости). --- ## Фаза 3 — Миграция вызывающих (depends Фаза 1-2) ### 6. routes/api_test.py - CMDB delete ОСТАЁТСЯ как предпроверка ПЕРЕД executor: при cmdb_ok → early return (opUid="cmdb-...", без /run, без поллинга). Иначе → execute_operation. - Остальные операции: заменить inline флоу на execute_operation. - _finish_op использует poll_until_done (save_run, _op_results, tracker_remove(delete) остаются здесь). - Импорт find_uid/uid_from_location из api/utils.py; удалить локальные копии. ### 7. operations/scenario.py - Заменить inline флоу на execute_operation. - sync while-поллинг → poll_until_done. - Удалить локальные _find_uid/_uid_from_location. ### 8. db/init_db.py — startup cleanup (E16) В init_db() добавить: UPDATE scenario_runs SET status='TIMEOUT', error_log='worker restart' WHERE status='RUNNING' AND created_at < NOW() - INTERVAL '1 hour'; (init_db вызывается из pool._ensure_schema() раз на воркер, идемпотентно). --- ## Фаза 4 — UI редактора (depends Фаза 2) ### 9. static/js/params-render.js (NEW) Вынести из operations.js: - renderParamRow(p, allInst) — уже чистая, без глобалов. - renderMapFixedRow(p, dfl) — чистая. - collectParams(containerSelector='#params-form') — параметризовать контейнер (единственная правка сигнатуры; сейчас хардкодит #params-form). Глобалы AUTOTEST_PREFIX/currentSvcId/makeCreateDisplayName остаются в operations.js. _esc (utils.js) и validateJson — общие глобалы. ### 10. static/js/operations.js Использовать общий params-render.js (удалить дубли рендера). ### 11. static/js/scenario-form.js — модальный редактор - Модал на весь экран (не inline scenario-body — параметры map-fixed слишком тесны). - Дропдаун сервисов (GET /api/services). - Дропдаун операций (GET /api/operations/{svcId}). - Авто pre-fill параметров при смене операции (GET /api/params/{svcOpId}), показать ВСЕ параметры с defaults + кнопка «Сбросить на defaults». - Поле output для create-шагов. - Дропдаун instance_ref для не-create (output'ы предыдущих шагов). - Кнопки [↑][↓] перестановки шагов. - В БД — символические имена параметров ({"durationMs":"5000"}), резолв в numeric ID в runtime. ### 12. templates/index.html Разметка модала + подключить params-render.js. ### 13. app.py — bump VERSION. --- ## Решения (зафиксировано) - Схема БД без изменений (C9), только новые ключи в steps JSONB. - Обе версии формата параллельно, без миграции данных (B8), fallback на service_id. - _op_results в памяти — вне scope (E18). - lock_check (один RUNNING сценарий) сохраняется (E17). - instance_bindings (JSONB) — используется (C10). ## Verification 1. py_compile для .py, node -c для .js. 2. Ручной CREATE через UI → инстанс создан, в трекере, запись в runs. 3. Ручной DELETE → CMDB-путь работает (early return). 4. Сценарий старый формат (seed dummy_test) → работает без изменений. 5. Сценарий новый формат: create output:d1 → modify instance_ref:d1 → delete instance_ref:d1 → все OK, instance_bindings заполнен. 6. Два create одного сервиса с разными output → два разных инстанса. 7. Валидация: instance_ref на несуществующий output → ошибка при сохранении. 8. Startup-cleanup: зависший RUNNING >1ч → TIMEOUT после рестарта. ## Файлы NEW: operations/executor.py, operations/poll.py, api/utils.py, static/js/params-render.js MOD: routes/api_test.py, operations/scenario.py, routes/api_scenario_defs.py, db/init_db.py, static/js/scenario-form.js, static/js/operations.js, templates/index.html, app.py (VERSION)