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

41 KiB
Raw Permalink 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.


Универсальный генератор моков из STANDS YAML (Опус, 2026-07-31)

Вердикт: универсальный конвертер возможен для всех 37 сервисов

Опус изучил 6 STANDS YAML (dummy, postgres, k8s, mariadb, VM v2, vDC). Структура единообразна — генерятся одним приложением. STANDS — авторитетный источник (сверен с HAR: create=18, параметры совпадают 1:1).

Ключевой маппинг STANDS → polygon

STANDS Polygon Тип
service_idserviceId, service_short_namesvcShort 1:1
operations[].id/name/actionsvcOperationId/operation/isCreate 1:1
params[].id/code/default/requiredsvcOperationCfsParamId/svcOperationCfsParam/defaultValue/isRequired 1:1
params[].data_typedataType html.unescape
params[].sub_params (list) → dataDescriptor (dict по code) трансформация
operations[].kind: subresourcestateOut[plural(subresource)] трансформация
name/service_man/lifecycle/outputs/func/descr/man/sort игнор

Алгоритм convert_one()

  1. Базовые поля + операции (фильтр kind: instance/action)
  2. cfsParams из create-параметров: unescape, sub_params→dataDescriptor (рекурсивно), default→default_for(dataType)
  3. stateParams: scalar→строка, map-fixed→json.dumps({sub_key: sub_val})
  4. stateOut: subresource-операции → {plural(subresource): {}}

4 границы универсальности

  1. Контент stateOut — структура (users/databases) генерится, имена (pgadmin/mydb) — нет. Решение: мок реализует subresource-операции
  2. redeploy не у всех — конвертер включает что есть
  3. Динамический valueList (func: getAvailableResourceRealms) — берём статический
  4. Битые valueList — pass-through

Архитектура polygon/from_stands.py

convert_all(stands_dir) → dict[int, config]
  convert_one(raw) → config
    _map_param, _build_dd, _gen_state_params, _gen_state_out

Поток: STANDS/*.yamlyaml.safe_loadconvert_one → MockState. Вызывается при СТАРТЕ полигона. Ручные YAML (polygon/services/) больше не нужны.

Subresource-операции в MockState (Опус)

Обычные instanceOperations, без отдельного типа. Разница только в apply_effect:

  • action=="create" + флаг subresource → взять имя из params (username/dbName), instance.stateOut[subresource][name] = {}
  • action=="delete" + subresourcedel instance.stateOut[subresource][name]
  • иначе → прежняя логика (modify→stateParams, delete→удаление, suspend/resume→статус)

Конвертер помечает такие операции: {svcOperationId, operation, subresource, action}. Один универсальный apply_effect, без сервис-специфичного кода.


Полигон — отдельная репа (2026-07-31)

Решение: отдельный managed-сервис

Полигон выносится из app-autotest/polygon/ в отдельный репозиторий https://gitea.services.ngcloud.ru/forcloud/polygon.git.

Это НЕ часть app-autotest, а самостоятельный сервис: polygon.pythonk8s.dev.nubes.ru/ — эмулятор Nubes API.

Структура (Nubes-совместимая, по howto-flask-nubes.md)

polygon/
├── requirements.txt
├── README.md
├── .gitignore
└── site/
    ├── app.py            # точка входа, порт 5000
    ├── state.py          # MockState
    ├── config_loader.py  # загрузка YAML + defaults
    ├── defaults.py       # default_for(dataType)
    ├── from_stands.py    # конвертер STANDS YAML → polygon/services/*.yaml
    └── services/         # генерится конвертером

Поправки к плану Опуса

Было Стало
polygon/server.py на 5001 site/app.py на 5000
Папка внутри app-autotest Отдельная репа
NUBES_API_ENDPOINT=http://localhost:5001 http://localhost:5000 (или URL сервиса)
Только локально/CI Можно задеплоить на Nubes

Ключевые правила

  • polygon/ репа = только код, никакой документации
  • Документация полигона → HISTORY/2026-07-31-session.md (этот файл)
  • polygon/ добавлен в .gitignore основного репо
  • Версионирование своё: polygon v0.1.0
  • YAML не руками — через from_stands.py из STANDS (37 сервисов)
  • site/__init__.py нельзя
  • Импорты без префикса site.

Полигон — реализация (2026-07-31, DeepSeek Pro 4)

Архитектура утверждена (Sonnet 4.6)

Промпт Соннету в polygon-docs/sonnet-polygon-prompt.md, ответ в polygon-docs/sonnet-response.md. Ссылки на генератор YAML: polygon-docs/tf-provider-refs.md.

Ключевые решения Соннета (отличия от Опуса):

  • subresources: kind == "subresource" из STANDS YAML
  • dtFinish: синхронный в /run (sleep → apply → dtFinish)
  • Лишние операции (restart, recovery): включать все, no-op
  • stateOut: авто-генерация из subresource-операций
  • map с sub_params → dataDescriptor для всех типов
  • 11 тестов (против 5 у Опуса)
  • UUID: uuid.uuid4()
  • Mock-эндпоинты: +/_mock/state, /_mock/services, /_mock/delay

Уточнения:

  • Синхронный dtFinish (блокирует воркер, но MOCK_OP_DELAY ≤ 0.5s)
  • Вариант Б: сгенерированные YAML коммитятся в репу (НЕ генерить на лету)
  • from_stands.py <STANDS_DIR> — без хардкода, читает все .yaml из директории

Сервис запущен на Nubes

  • URL: https://polygon.pythonk8s.dev.nubes.ru/
  • Репа: https://gitea.services.ngcloud.ru/forcloud/polygon.git
  • InstanceUid: db9d7835-1ef8-4fee-b42f-c91e1c0783cb
  • Мониторинг: Grafana (namespace db9d7835...)

Этап 1 — from_stands.py (77f12d2)

Создан универсальный конвертер STANDS YAML → polygon config:

  • html.unescape() для всех строк (&gt;>, &quot;")
  • Рекурсивный dataDescriptor из sub_params (для map, map-fixed, array-map-fixed)
  • Авто-генерация state_out_template из subresource-операций
  • stateParams из create-операции с JSON-генерацией для map-fixed
  • cfsParamsByOp — связка opId → список paramId
  • 210 строк, 37 YAML сгенерировано, 0 ошибок
  • Все YAML валидны, HTML entities раскодированы

Этап 2 — config_loader + mock_state + state_machine (1956fb7)

config_loader.py (50 строк):

  • Загружает все YAML из services/, строит {svcId: def} + {opId: def}
  • 37 сервисов, 245 операций в индексе

mock_state.py (150 строк):

  • MockState синглтон: instances, operations, op_params
  • create_instance() → UUID v4, статус "creating"
  • create_operation() → UUID v4, dtFinish=None
  • list_instances() с пагинацией (pageSize≤200, стоп по len<pageSize)
  • set_param(), get_params(), reset()

state_machine.py (140 строк):

  • apply_effect() — мутирует MockState по kind/action:
    • instance+create → статус running, stateParams из шаблона, stateOut из шаблона
    • instance+modify → мерж params через cfsParamsByOp
    • instance+delete → удалить инстанс
    • instance+suspend/resume/redeploy → статус
    • subresource+create → state.out[plural][name] = {}
    • subresource+delete → del state.out[plural][name]
  • _extract_subresource_name() — ищет имя в op_params, fallback на subresource_name

Этап 3 — app.py: 17 эндпоинтов (d4c2a26)

# Метод Путь Детали
1 GET /health "OK"
2 GET / HTML: версия, счётчики, delay
3 GET /api/v1/svc/services список всех сервисов
4 GET /api/v1/svc/services/<id> операции сервиса
5 GET /api/v1/svc/instances пагинация (pageSize, page)
6 GET /api/v1/svc/instances/<uid> полный instance+state
7 POST /api/v1/svc/instances 201 + Location: ./{uid}
8 GET /api/v1/svc/instanceOperations/default/<id> cfsParams с dataDescriptor
9 POST /api/v1/svc/instanceOperations 201 + Location, auto-find create opId
10 GET /api/v1/svc/instanceOperations/<uid> +cfsParams если ?fields=...
11 POST /api/v1/svc/instanceOperationCfsParams paramId → value
12 GET /api/v1/svc/instanceOperations/<uid>/validate-cfs пустое тело, 200
13 POST /api/v1/svc/instanceOperations/<uid>/run sleep(delay) → apply_effect → dtFinish
14 POST /api/v1/svc/_mock/reset сброс состояния
15 GET /api/v1/svc/_mock/state отладка: instances + operations
16 GET /api/v1/svc/_mock/services отладка: все сервисы
17 POST /api/v1/svc/_mock/delay/<s> изменить MOCK_OP_DELAY

Критические детали:

  • default/<int:op_id> строго ДО <uid> в маршрутах Flask
  • validate-cfs: return "", 200 (НЕ jsonify)
  • _now(): ISO-формат с 'Z'

v0.1.0 → v0.2.0 (4e136cd)

Bump версии после трёх этапов изменений.

Этап 4 — тесты (760d16e)

test_converter.py (10 тестов):

  • basic_fields, lifecycle, operations_count
  • html_unescape, value_list, state_params_from_create
  • data_descriptor_from_sub_params, state_out_subresources
  • cfs_params_by_op, roundtrip (запись/чтение YAML)

test_state_machine.py (9 тестов):

  • create → running + params + state.out
  • delete → инстанс удалён
  • suspend → suspended
  • resume → running
  • modify → params мержатся
  • create_user → state.out.users[name]
  • delete_user → users[name] удалён
  • reset → всё пусто

Все 19 тестов PASS.

Проверка в кубере

ssh naeel@5.172.178.213 kubectl -n db9d7835... logs deploy/pythonk8s --tail=20
  • Под: 1/1 Running, перезапущен
  • Логи: все curl-запросы (201, 200), ни одной ошибки

Проверка curl (deployed)

curl https://polygon.pythonk8s.dev.nubes.ru/health → OK
37 services, create flow: 201+Location → run → status=running, params=12

Файлы polygon-docs/ (в корне autotest)

  • sonnet-polygon-prompt.md — промпт для Claude Sonnet 4.6
  • sonnet-response.md — полный ответ Соннета (архитектура + план)
  • tf-provider-refs.md — ссылки на генератор YAML в ~/tf_provider

Итоговая структура polygon

polygon/
├── requirements.txt
├── README.md
├── .gitignore
├── tests/
│   ├── test_converter.py       # 10 тестов
│   └── test_state_machine.py   # 9 тестов
└── site/
    ├── app.py                  # 17 эндпоинтов
    ├── mock_state.py           # MockState (in-memory)
    ├── state_machine.py        # apply_effect()
    ├── config_loader.py        # загрузка YAML
    ├── from_stands.py          # конвертер STANDS → polygon
    └── services/               # 37 сгенерированных YAML

~900 строк Python, 37 YAML-конфигов, 19 тестов PASS. Задеплоено, проверено через curl и kubectl. Готово к интеграции с app-autotest.


Интеграция polygon ↔ app-autotest (2026-07-31, Sonnet + DeepSeek)

Промпт Соннету

Сохранён в polygon-docs/sonnet-integration-prompt.md. 6 вопросов.

Ответ Соннета

Подход: endpoint in STANDS вместо _is_localhost().

Список STANDS уже есть в http_client.py:

STANDS = [
    "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc",
    "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc",
]

Если NUBES_API_ENDPOINT — один из STANDS → автоопределение как раньше. Любой другой URL (localhost, polygon, кастом) → использовать напрямую, без detect.

Для stand_name: https://polygon.pythonk8s.dev.nubes.ru содержит "dev" → stand_name вернёт "dev" автоматически. Для localhost → "?" заменяется на "mock".

Коллизия портов: polygon и app-autotest оба на 5000 локально. Решение: polygon на 5001.

Реализация (v1.2.24)

Файл: app-autotest/site/api/auth.py (единственный файл).

Изменения:

  1. Импорт: from api.http_client import ..., STANDS
  2. get_client(): if endpoint in STANDS: detect_endpoint(...) — автоопределение только для известных стендов
  3. get_stand(): if endpoint not in STANDS: s = stand_name(endpoint); return s if s != "?" else "mock"

Использование:

# Локально
NUBES_API_ENDPOINT=http://localhost:5001/api/v1/svc python app.py

# Против задеплоенного polygon
NUBES_API_ENDPOINT=https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc python app.py

# Реальные стенды — без изменений
python app.py  # NUBES_API_ENDPOINT = lk-api-gateway-test... → автоопределение

Версия: app-autotest v1.2.23 → v1.2.24 (a398892).


Финальные доработки polygon (2026-07-31, Fable 5 + DeepSeek)

Аудиты

  • Опус: архитектурный аудит → 5 находок. Главное: публичный деплой vs single-user дизайн.
  • Fable 5: bug hunt → 7 находок. Нашёл то что 3 других ревью пропустили: setdefault не работает, sleep блокирует /health, int(param_id) без try.
  • Моё мнение: Fable vs Opus — polygon-docs/my-opinion-fable-vs-opus.md
  • Гайд по AI-моделям: polygon-docs/ai-models-guide.md

Исправлено (v0.2.5 → v0.3.1)

Фикс От кого
Authorization: Bearer на ВСЕХ эндпоинтах (если MOCK_AUTH_TOKEN задан) Fable
Cap DELAY ≤ 5с, min ≥ 0, float() с try/except Fable
Runtime warning если WEB_CONCURRENCY != 1 (setdefault удалён) Fable
int(param_id) с try/except — нет 500 Fable
/_mock/fail-next — симуляция падения (isSuccessful=False) Fable
.format()%s в докстринге (KeyError fix) Я

Интеграционные тесты (app-autotest v1.2.25)

test_polygon_integration.py — 15 тестов (все PASS):

  • services CRUD, create flow, postgres state.out
  • 409 при повторном run, modify-мерж, suspend/resume, delete
  • пагинация, _mock/state, _mock/services, _mock/delay

Оптимизация токенов

  • copilot-instructions.md: 106 → 25 строк (выброшены повторы, прецеденты)
  • Гайд по моделям: короткие промпты для Fable (40 строк), полные для Опуса (150 строк)

Итог

  • Polygon: v0.3.1, задеплоен, 23/23 curl PASS
  • App-autotest: v1.2.25, STANDS-check интеграция
  • Готов к последовательному CI.
  • Блокеры для параллельного CI: per-session модель (Опус), симуляция ошибок (Fable)