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

230 lines
12 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.
# Ответ Claude Sonnet 4.6 — архитектура и план polygon
Дата: 2026-07-31
---
## Ответы на Q1–Q10
**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: UUID**`uuid.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]`)
---
## Схема данных в памяти
```python
# 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:**
```python
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()`.