Files
autotest/polygon-docs/sonnet-response.md
T

12 KiB
Raw Blame History

Ответ Claude Sonnet 4.6 — архитектура и план polygon

Дата: 2026-07-31


Ответы на Q1Q10

Q1: map-fixed → dataDescriptor Конвертер ГЕНЕРИРУЕТ dataDescriptor из sub_params. Формат каждой записи:

{sub_param_code: {dataType, defaultValue, valueList, isRequired}}

Это обязательно: normalize_value в app-autotest использует dataDescriptor для генерации JSON из defaults при пустом значении map-параметра.

Q2: subresources Правило простое и точное: kind == "subresource" (это явное поле в STANDS YAML). apply_effect смотрит на kind:

  • kind == "instance" → меняем статус + state.params
  • kind == "subresource" → меняем state.out[plural], статус НЕ меняем

Q3: stateOut auto-generation Да, конвертер выводит автоматически. Сканирует операции с kind=subresource, action=create, строит state_out_template. Пример для postgres: {users: {}, databases: {}, backups: {}}. При create инстанса мок копирует этот шаблон в instance.state.out.

Q4: "лишние" операции Включить все (restart, recovery, etc.) — иначе GET /instanceOperations/default/{id} вернёт 404 и app-autotest упадёт. Реализация: no-op transition (статус остаётся running, dtFinish ставится, isSuccessful=True). Если operation_id вообще не найден → 404 с {"error": "operation not found"}.

Q5: valueList Хранить и отдавать обязательно. get_params_with_current_values в app-autotest заполняет из него дропдауны для delete_user/delete_database.

Q6: Подход A (generate YAML → services/) Выбрать A. from_stands.py генерирует site/services/N_name.yaml, config_loader.py читает при старте Flask. Плюс: ручная правка для сложных случаев, прозрачность. from_stands.py запускается один раз при setup.

Q7: Mock-эндпоинты Все 4:

  • POST /_mock/reset — обязателен для тестовой изоляции
  • GET /_mock/state — текущее состояние (отладка)
  • GET /_mock/services — список загруженных сервисов
  • POST /_mock/delay/<float> — изменить MOCK_OP_DELAY на лету

Q8: UUIDuuid.uuid4(). Детерминированные не нужны.

Q9: Пагинация → stop when len(batch) < pageSize, cap at 200, page=1 by default. Response: {"results": [...], "pageSize": N, "page": P, "total": len(all)}.

Q10: Тесты К 5 сценариям Опуса добавить: (a) тест пагинации (3 инстанса, pageSize=2), (b) тест subresource create→verify state.out→delete→verify removed, (c) тест modify→verify params merged, (d) тест /_mock/state, (e) тест 404 на невалидный op_id. Каждый тест начинается с POST /_mock/reset (через autouse fixture).


Структура файлов

polygon/
├── requirements.txt
└── site/
    ├── app.py              # Flask app + все 15 маршрутов (один файл)
    ├── mock_state.py       # MockState: instances, operations, op_params
    ├── state_machine.py    # apply_effect()
    ├── config_loader.py    # load_services() → services_dict + ops_index
    ├── from_stands.py      # CLI-конвертер STANDS YAML → services/*.yaml
    ├── services/           # 37 YAML-файлов (generated)
    └── templates/
        └── index.html

Ключевое ограничение Nubes:

  • site/__init__.py — ЗАПРЕЩЁН
  • from site.xxx import — ЗАПРЕЩЁН
  • В app.py: только import mock_state, import config_loader, import state_machine (работает т.к. Python добавляет директорию скрипта в sys.path[0])

Схема данных в памяти

# mock_state.py — синглтон state = MockState()
class MockState:
    instances  = {}   # instanceUid → instance_dict
    operations = {}   # opUid → operation_dict
    op_params  = {}   # opUid → {paramId(int): paramValue(str)}

instance_dict: {instanceUid, serviceId, displayName, descr, status, explainedStatus, svc, dtCreate, state: {params: {code: val}, out: {users: {}, databases: {}}}}

operation_dict: {instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, subresource|None, dtStart, dtFinish|None, isSuccessful|None, errorLog, svc, stages}


Конвертер from_stands.py

HTML entities: STANDS YAML содержит integer &gt;= 0, &quot; и т.д. from_stands.py обязан применять html.unescape() ко всем строковым полям (data_type, value_list элементы, default).

Маппинг STANDS → cfsParam формат:

STANDS polygon/cfsParam
id svcOperationCfsParamId
code svcOperationCfsParam
data_type (unescape) dataType
required isRequired
default defaultValue
value_list valueList
sub_params (map-fixed) dataDescriptor: {code: {dataType,defaultValue,valueList,isRequired}}
sub_params (map) sub_params (хранить как есть для normalize_value)

stateOut auto-generation:

for op in operations:
    if op.kind == "subresource" and op.action == "create":
        plural = op.subresource + "s"  # user→users, database→databases
        state_out_template[plural] = {}

Полный список API эндпоинтов (15 шт.)

# Метод Путь Назначение
1 GET /health "OK"
2 GET / HTML-страница
3 GET /api/v1/svc/instances Список инстансов (paged)
4 GET /api/v1/svc/instances/<uid> Детали инстанса
5 POST /api/v1/svc/instances Создать shell → 201 + Location
6 GET /api/v1/svc/instanceOperations/default/<int:op_id> Шаблон операции
7 POST /api/v1/svc/instanceOperations Создать операцию → 201 + Location
8 GET /api/v1/svc/instanceOperations/<uid> Детали операции + cfsParams
9 POST /api/v1/svc/instanceOperationCfsParams Задать значение параметра
10 GET /api/v1/svc/instanceOperations/<uid>/validate-cfs Валидация → 200 пустое тело
11 POST /api/v1/svc/instanceOperations/<uid>/run Выполнить операцию
12 POST /api/v1/svc/_mock/reset Сброс состояния
13 GET /api/v1/svc/_mock/state Debug: текущее состояние
14 GET /api/v1/svc/_mock/services Debug: загруженные сервисы
15 POST /api/v1/svc/_mock/delay/<float:s> Изменить MOCK_OP_DELAY

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

  • Эндпоинт 5 (POST /instances): возвращает 201 + заголовок Location: ./{uid}. HttpClient.post() берёт uid из последнего сегмента Location, проверяет len >= 32. Body: {"instanceUid": uid} (двойная защита).

  • Эндпоинт 7 (POST /instanceOperations): при operation=="create" тело НЕ содержит svcOperationId — polygon находит его сам из svc_def.operations["create"]["id"].

  • Эндпоинт 10 (validate-cfs): return "", 200 (НЕ jsonify). app-autotest ловит JSONDecodeError и считает это успехом.

  • Эндпоинт 11 (run): синхронный — time.sleep(MOCK_OP_DELAY), затем apply_effect, затем dtFinish = datetime.utcnow().isoformat() + "Z". Первый poll после run увидит dtFinish.

  • Маршруты Flask: /instanceOperations/default/<int:op_id> должен стоять выше /instanceOperations/<uid> в app.py.


Поток запроса (полный цикл create)

executor.py                              polygon
─────────                                ───────
POST /instances {serviceId,displayName}
                                         → создать instance shell (status="creating")
                                         → response 201 + Location: ./uid1
← instanceUid = uid1

POST /instanceOperations {instanceUid, operation:"create"}
                                         → найти create op_id из service_def
                                         → создать operation_dict (dtFinish=None)
                                         → response 201 + Location: ./uid2
← opUid = uid2

GET /instanceOperations/uid2?fields=cfsParams
                                         → response {"instanceOperation": {"cfsParams": [...]}}
                                           cfsParams из service_def + пустые paramValue

POST /instanceOperationCfsParams × N    → state.op_params[uid2][paramId] = val
GET /instanceOperations/uid2/validate-cfs → response "" 200
POST /instanceOperations/uid2/run
                                         → sleep(0.1s)
                                         → apply_effect: state.params←codes, status="running"
                                         → dtFinish = now
                                         → response {"ok": true}

GET /instanceOperations/uid2?fields=dtFinish,...
                                         → response {"instanceOperation": {"dtFinish": "2026-...", "isSuccessful": true, ...}}
← poll done: status="OK"

Фазы реализации

Этап 1 — Конвертер (from_stands.py): html.unescape, convert_param рекурсивный, build_state_out_template, pluralize, CLI. Запустить: cd polygon/site && python from_stands.py. Критерий: 37 YAML в site/services/, у postgres state_out_template: {users:{}, databases:{}, backups:{}}.

Этап 2 — MockState + config_loader + state_machine: три отдельных модуля. Критерий: python -c "import config_loader; s,i=config_loader.load_services('services'); print(len(s))" → 37.

Этап 3 — Flask API в app.py: реализовать все 15 эндпоинтов в порядке от простых к сложным (см. таблицу). Критерий: python -m py_compile app.py без ошибок, curl localhost:5000/health → OK.

Этап 4 — Тесты:

  • В polygon: tests/test_converter.py, tests/test_state_machine.py (юнит)
  • В app-autotest: tests/conftest.py (+fixture polygon_server subprocess), tests/test_polygon_integration.py (интеграционные)

Уточнения (второй раунд)

Уточнение 1: map с sub_params → тоже dataDescriptor

Правило: если у параметра есть sub_params — генерировать dataDescriptor, независимо от того map это или map-fixed.

data_type sub_params Действие
map-fixed есть генерировать dataDescriptor
map есть генерировать dataDescriptor
array-map-fixed есть генерировать dataDescriptor
любой нет dataDescriptor: null

Уточнение 2: state.out после create — пустые {}

Достаточно пустых {} из state_out_template. STANDS YAML не описывает runtime-дефолты (pgadmin, mydb) — это эффект реального Terraform, не мок.

Уточнение 3: расположение тестов

app-autotest/
  tests/
    conftest.py                      # + fixture polygon_server (subprocess)
    test_polygon_integration.py      # интеграционные тесты

polygon/
  tests/
    test_converter.py                # юнит: from_stands.py
    test_state_machine.py            # юнит: apply_effect

polygon_server fixture: subprocess → poll /health (timeout 5s) → после тестов terminate().