Sprint 0: OpenAPI 3.1 контракт API v1 + генерация TypeScript-клиента #19

Closed
opened 2026-10-21 23:24:17 +03:00 by embeddevio · 4 comments
Owner

Owner: backend-engineer · Est: 3 SP · Priority: P0 · Sprint: 0 · Deps: —
Refs #15

Что сделать

  • OpenAPI 3.1 спецификация как source of truth: GET/PUT /api/v1/status, PUT /api/v1/config, POST /api/v1/config/validate, GET/POST/DELETE /api/v1/routing/rules, PUT /api/v1/routing/mode, POST /api/v1/routing/geoip/update, POST /api/v1/routing/import, GET /api/v1/metrics/stream (WS), плюс /api/v1/health, /metrics, /api/v1/devices, /api/v1/logs, /api/v1/auth/*.
  • Схемы ошибок единообразны ({code, message, details}), все поля секретов помечены writeOnly.
  • Генерация TypeScript-клиента для ui/ и тест «схема ↔ реализация» (контрактный тест в CI).

Acceptance criteria

  • GIVEN спецификация, WHEN запускаю генератор, THEN TS-клиент создаётся без ошибок и коммитится в ui/src/api/generated/.
  • GIVEN реализованный эндпоинт с отклонением от схемы, WHEN запущен контрактный тест, THEN CI падает с указанием расхождения.
  • GIVEN PrivateKey/PresharedKey в схеме, WHEN смотрю спецификацию, THEN поля writeOnly, readOnly-ответов с ключами нет.

DoD

Тесты на новый код ≥ 70 %, CI green, PR с ревью владельца роли.

**Owner:** backend-engineer · **Est:** 3 SP · **Priority:** P0 · **Sprint:** 0 · **Deps:** — Refs #15 ### Что сделать - OpenAPI 3.1 спецификация как source of truth: `GET/PUT /api/v1/status`, `PUT /api/v1/config`, `POST /api/v1/config/validate`, `GET/POST/DELETE /api/v1/routing/rules`, `PUT /api/v1/routing/mode`, `POST /api/v1/routing/geoip/update`, `POST /api/v1/routing/import`, `GET /api/v1/metrics/stream` (WS), плюс `/api/v1/health`, `/metrics`, `/api/v1/devices`, `/api/v1/logs`, `/api/v1/auth/*`. - Схемы ошибок единообразны (`{code, message, details}`), все поля секретов помечены `writeOnly`. - Генерация TypeScript-клиента для `ui/` и тест «схема ↔ реализация» (контрактный тест в CI). ### Acceptance criteria - GIVEN спецификация, WHEN запускаю генератор, THEN TS-клиент создаётся без ошибок и коммитится в `ui/src/api/generated/`. - GIVEN реализованный эндпоинт с отклонением от схемы, WHEN запущен контрактный тест, THEN CI падает с указанием расхождения. - GIVEN `PrivateKey`/`PresharedKey` в схеме, WHEN смотрю спецификацию, THEN поля `writeOnly`, `readOnly`-ответов с ключами нет. ### DoD Тесты на новый код ≥ 70 %, CI green, PR с ревью владельца роли.
embeddevio added this to the Sprint 0 — Фундамент: монорепо, CI, Docker-образ milestone 2026-10-21 23:24:17 +03:00
embeddevio added the backendstatus/ready labels 2026-10-21 23:24:17 +03:00
Author
Owner

TechLead — разбор блокировки задачи (22.10, 00:14).

Оба падения воркера по #19 (run #5 — 1227 с, run #10 — 123 с, оба rc=1) вызваны не задачей: в 23:52–23:55 провайдер отвергал все запросы к deepseek/deepseek-v4.1-flash из-за исчерпанного дневного лимита ключа polza. Разбор и решения по бюджету — эскалация #51. Ничего по контракту опубликовано не было (в issue нет ни комментариев, ни ветки в PR).

Сделано: карточка t_36a212ed разблокирована и перезапущена (run #13, старт 22.10 00:08, профиль backend-engineer).

Напоминание воркеру (из истории инцидентов): ветка feat/19-openapi — строго от main; создание ветки от другой feature-ветки уже один раз привело к закрытию PR #48 (в PR тянулись чужие коммиты).

DoD без изменений: coverage ≥70 %, ревью владельца роли, Refs #19 и Refs #15. CI green станет достижим только после появления runner'а (#52).

**TechLead — разбор блокировки задачи (22.10, 00:14).** Оба падения воркера по #19 (run #5 — 1227 с, run #10 — 123 с, оба `rc=1`) вызваны не задачей: в 23:52–23:55 провайдер отвергал все запросы к `deepseek/deepseek-v4.1-flash` из-за исчерпанного дневного лимита ключа polza. Разбор и решения по бюджету — эскалация #51. Ничего по контракту опубликовано не было (в issue нет ни комментариев, ни ветки в PR). **Сделано:** карточка `t_36a212ed` разблокирована и перезапущена (run #13, старт 22.10 00:08, профиль `backend-engineer`). **Напоминание воркеру (из истории инцидентов):** ветка `feat/19-openapi` — строго от `main`; создание ветки от другой feature-ветки уже один раз привело к закрытию PR #48 (в PR тянулись чужие коммиты). **DoD без изменений:** coverage ≥70 %, ревью владельца роли, `Refs #19` и `Refs #15`. CI green станет достижим только после появления runner'а (#52).
Author
Owner

TechLead — возобновление работы (28.10, 20:5x).

Провайдер восстановлен: ключ polza был деактивирован (HTTP 403: API ключ заблокирован или деактивирован), воркеры выходили rc=78 (terminal provider error) — после ротации ключа владельцем всё работает, дневной лимит 400 ₽.

Состояние работы по #19 на момент паузы: в ветке feat/19-openapi (HEAD 95f1337fd4f33550586a0cbd87214c23f1a808b7) уже лежит полный контракт и код (docs/openapi.yaml 36 КБ, backend/awg_api/*, 3 файла тестов, ui/scripts/generate-api.sh, requirements-contract.txt, .gitea/workflows/contract-openapi.yml), но PR не создан — run упал ровно на шаге публикации, отчёта в issue тоже нет.

Сделано: карточка t_36a212ed разблокирована и перезапущена с точной картой уже опубликованного (чтобы не делать работу заново) и с чек-листом до AC/DoD: PR в main из этой ветки, реальные прогоны контрактного теста и генератора TS-клиента с выводом в комментарии, Refs #19/Refs #15.

CI: по ветке висят runs #2–#7 — все completed/cancelled, ни один job не исполнялся (runner'а нет, блокер #52). Гейты гоняются локально, CI-статус в PR будет зелёным только после runner'а.

**TechLead — возобновление работы (28.10, 20:5x).** Провайдер восстановлен: ключ polza был деактивирован (`HTTP 403: API ключ заблокирован или деактивирован`), воркеры выходили `rc=78` (terminal provider error) — после ротации ключа владельцем всё работает, дневной лимит 400 ₽. **Состояние работы по #19 на момент паузы:** в ветке `feat/19-openapi` (HEAD `95f1337fd4f33550586a0cbd87214c23f1a808b7`) уже лежит полный контракт и код (docs/openapi.yaml 36 КБ, backend/awg_api/*, 3 файла тестов, ui/scripts/generate-api.sh, requirements-contract.txt, .gitea/workflows/contract-openapi.yml), но **PR не создан** — run упал ровно на шаге публикации, отчёта в issue тоже нет. **Сделано:** карточка `t_36a212ed` разблокирована и перезапущена с точной картой уже опубликованного (чтобы не делать работу заново) и с чек-листом до AC/DoD: PR в main из этой ветки, реальные прогоны контрактного теста и генератора TS-клиента с выводом в комментарии, `Refs #19`/`Refs #15`. **CI:** по ветке висят runs #2–#7 — все `completed/cancelled`, ни один job не исполнялся (runner'а нет, блокер #52). Гейты гоняются локально, CI-статус в PR будет зелёным только после runner'а.
embeddevio added status/in-progress and removed status/ready labels 2026-10-28 20:57:08 +03:00
Author
Owner

Отчёт backend-engineer по #19 — PR #57 (ветка feat/19-openapi)

Refs #19 · Refs #15

PR: #57 (base main, 25 файлов, +3310/-0, mergeable).
Ветка от main (без чужих коммитов от других feature-веток).

Что готово

  • docs/openapi.yaml — OpenAPI 3.1, 1254 строки, все эндпоинты из ТЗ + /api/v1/health, /ready, /metrics, /api/v1/devices, /api/v1/logs, /api/v1/auth/*, WS /api/v1/metrics/stream.
  • Единый формат ошибок {code,message,details}, закрытый enum ErrorCode; любой не-2xx ответ обязан ссылаться на Error (тест test_every_non_2xx_response_uses_error_schema).
  • private_key / preshared_key / password — writeOnly: true в спеке и в pydantic-моделях; схемы ответов приватных полей не содержат.
  • Мок-контрол-плейн backend/awg_api/**; /openapi.json отдаётся из YAML, а не генерится FastAPI — контракт остаётся источником истины.
  • TS-клиент ui/src/api/generated/schema.d.ts (39 455 B) сгенерирован openapi-typescript@7.13.0 и закоммичен.
  • CI: .gitea/workflows/contract-openapi.yml (job contract — ruff/mypy/pytest; job ts-client — перегенерация + git diff --exit-code).

Гейты — локальный прогон (Gitea Actions runner недоступен, #52)

ruff check backend           -> All checks passed!
ruff format --check backend  -> 17 files already formatted
mypy backend                 -> Success: no issues found in 22 source files
pytest                       -> 93 passed, coverage 95.48% (порог 70%)

AC

1. Генератор TS-клиента отрабатывает без ошибок — ВЫПОЛНЕНО

$ npx --yes openapi-typescript@7 docs/openapi.yaml -o ui/src/api/generated/schema.d.ts
openapi-typescript 7.13.0  -> docs/openapi.yaml -> schema.d.ts [118.2ms]
exit=0; 39455 bytes, 1229 lines; blob в ветке dec37349e9d1b6c1f28bd28894eaae1589450525
повторная генерация -> идентичный файл (idempotent)

2. Отклонение реализации/схемы роняет контрактный тест — ВЫПОЛНЕНО (мутация → прогон → откат, blob после отката совпал)

Мутация Результат
router /api/v1/status отдаёт поле, которого нет в схеме pytest exit=1, FAILED test_success_response_matches_spec[get-/api/v1/status-None]
у private_key в спеке убран writeOnly exit=1, FAILED test_secret_fields_are_write_only, test_write_only_secrets_removed_from_response_schema, test_private_key_in_response_fails_validation
private_key утёк в ответ /api/v1/config exit=1, FAILED test_responses_contain_no_secret_material + валидация схемы
контроль: безобидная правка description в спеке exit=0 — тесты не «шумят» на косметике

3. В ответах нет приватных ключей — ВЫПОЛНЕНО — test_responses_contain_no_secret_material проверяет 7 ответов (/config GET+PUT, /status, /devices, /logs, /auth/session, /auth/logout) на отсутствие и сентинел-значений, и имён полей; схема ответа после вырезания writeOnly private_key/preshared_key не содержит.

DoD

  • coverage ≥ 70% (факт 95.48%)
  • CI green — НЕ достижим: runner не зарегистрирован (#52); гейты прогнаны локально
  • ревью владельца роли — ждёт PR #57

Важно для ревью

  • pyproject.toml / requirements-dev.txt приходят из #16 (PR #47 не влит) — до влития #47 job contract не стартует; файлы PR и #47 не пересекаются.
  • Тестовые файлы в ветке синхронизированы с пруф-прогоном (blob SHA 1555a68…, 4c6ba6e…), чтобы проверялся ровно тот код, что гонялся локально.
  • MockState — временное состояние до реального контрол-плейна Sprint 1+.

Прошлые run по этой задаче

Run 1–2 упали по контексту, run 3 — 403 от провайдера до создания PR, run 4 — инфраструктурный spawn_failed. Не переделывал работу с нуля: проверил дерево ветки по blob SHA, досинхронизировал 2 тест-файла и коммит сгенерированного клиента, прогнал все гейты и негативные проверки.

## Отчёт backend-engineer по #19 — PR #57 (ветка `feat/19-openapi`) Refs #19 · Refs #15 PR: http://192.168.1.111:3000/embeddevio/awg-dash/pulls/57 (base `main`, 25 файлов, +3310/-0, mergeable). Ветка от `main` (без чужих коммитов от других feature-веток). ### Что готово - `docs/openapi.yaml` — OpenAPI 3.1, 1254 строки, все эндпоинты из ТЗ + `/api/v1/health`, `/ready`, `/metrics`, `/api/v1/devices`, `/api/v1/logs`, `/api/v1/auth/*`, WS `/api/v1/metrics/stream`. - Единый формат ошибок `{code,message,details}`, закрытый enum `ErrorCode`; любой не-2xx ответ обязан ссылаться на `Error` (тест `test_every_non_2xx_response_uses_error_schema`). - `private_key` / `preshared_key` / `password` — `writeOnly: true` в спеке и в pydantic-моделях; схемы ответов приватных полей не содержат. - Мок-контрол-плейн `backend/awg_api/**`; `/openapi.json` отдаётся из YAML, а не генерится FastAPI — контракт остаётся источником истины. - TS-клиент `ui/src/api/generated/schema.d.ts` (39 455 B) сгенерирован `openapi-typescript@7.13.0` и закоммичен. - CI: `.gitea/workflows/contract-openapi.yml` (job `contract` — ruff/mypy/pytest; job `ts-client` — перегенерация + `git diff --exit-code`). ### Гейты — локальный прогон (Gitea Actions runner недоступен, #52) ``` ruff check backend -> All checks passed! ruff format --check backend -> 17 files already formatted mypy backend -> Success: no issues found in 22 source files pytest -> 93 passed, coverage 95.48% (порог 70%) ``` ### AC **1. Генератор TS-клиента отрабатывает без ошибок — ВЫПОЛНЕНО** ``` $ npx --yes openapi-typescript@7 docs/openapi.yaml -o ui/src/api/generated/schema.d.ts openapi-typescript 7.13.0 -> docs/openapi.yaml -> schema.d.ts [118.2ms] exit=0; 39455 bytes, 1229 lines; blob в ветке dec37349e9d1b6c1f28bd28894eaae1589450525 повторная генерация -> идентичный файл (idempotent) ``` **2. Отклонение реализации/схемы роняет контрактный тест — ВЫПОЛНЕНО** (мутация → прогон → откат, blob после отката совпал) | Мутация | Результат | |---|---| | router `/api/v1/status` отдаёт поле, которого нет в схеме | pytest exit=1, FAILED `test_success_response_matches_spec[get-/api/v1/status-None]` | | у `private_key` в спеке убран `writeOnly` | exit=1, FAILED `test_secret_fields_are_write_only`, `test_write_only_secrets_removed_from_response_schema`, `test_private_key_in_response_fails_validation` | | `private_key` утёк в ответ `/api/v1/config` | exit=1, FAILED `test_responses_contain_no_secret_material` + валидация схемы | | контроль: безобидная правка description в спеке | exit=0 — тесты не «шумят» на косметике | **3. В ответах нет приватных ключей — ВЫПОЛНЕНО** — `test_responses_contain_no_secret_material` проверяет 7 ответов (`/config` GET+PUT, `/status`, `/devices`, `/logs`, `/auth/session`, `/auth/logout`) на отсутствие и сентинел-значений, и имён полей; схема ответа после вырезания writeOnly `private_key`/`preshared_key` не содержит. ### DoD - [x] coverage ≥ 70% (факт 95.48%) - [ ] CI green — НЕ достижим: runner не зарегистрирован (#52); гейты прогнаны локально - [ ] ревью владельца роли — ждёт PR #57 ### Важно для ревью - `pyproject.toml` / `requirements-dev.txt` приходят из #16 (PR #47 не влит) — до влития #47 job `contract` не стартует; файлы PR и #47 не пересекаются. - Тестовые файлы в ветке синхронизированы с пруф-прогоном (blob SHA `1555a68…`, `4c6ba6e…`), чтобы проверялся ровно тот код, что гонялся локально. - `MockState` — временное состояние до реального контрол-плейна Sprint 1+. ### Прошлые run по этой задаче Run 1–2 упали по контексту, run 3 — 403 от провайдера до создания PR, run 4 — инфраструктурный spawn_failed. Не переделывал работу с нуля: проверил дерево ветки по blob SHA, досинхронизировал 2 тест-файла и коммит сгенерированного клиента, прогнал все гейты и негативные проверки.
Author
Owner

TechLead — PR #57 влит в main (28.10, 21:21).

Что в итоге в main: docs/openapi.yaml (1254 строки, 17 путей / 21 операция), мок-контрол-плейн backend/awg_api/**, 3 файла контрактных тестов, сгенерированный TS-клиент ui/src/api/generated/schema.d.ts, requirements-contract.txt, workflow .gitea/workflows/contract-openapi.yml.

Закрытые AC: генератор TS-клиента отрабатывает идемпотентно (blob сверен байт-в-байт); отклонение реализации от схемы роняет контрактный тест (проверено 3 мутациями + контроль); в ответах нет приватных ключей (writeOnly + отдельный probe по 7 ответам).

DoD: coverage 95.48 % (порог 70 %) — да; ревью владельца роли — да (независимый прогон ревьюером); CI green — нет, runner не зарегистрирован (#52) — это единственный незакрытый пункт, он трекается блокером #52 и будет закрыт первым же прогоном после регистрации runner'а.

Закрываю issue: задача выполнена и влита, остаточная зависимость от #52 общая для всего Sprint 0 и не является дефектом #19. Refs #15, #52.

**TechLead — PR #57 влит в `main` (28.10, 21:21).** Что в итоге в `main`: `docs/openapi.yaml` (1254 строки, 17 путей / 21 операция), мок-контрол-плейн `backend/awg_api/**`, 3 файла контрактных тестов, сгенерированный TS-клиент `ui/src/api/generated/schema.d.ts`, `requirements-contract.txt`, workflow `.gitea/workflows/contract-openapi.yml`. **Закрытые AC:** генератор TS-клиента отрабатывает идемпотентно (blob сверен байт-в-байт); отклонение реализации от схемы роняет контрактный тест (проверено 3 мутациями + контроль); в ответах нет приватных ключей (`writeOnly` + отдельный probe по 7 ответам). **DoD:** coverage 95.48 % (порог 70 %) — да; ревью владельца роли — да (независимый прогон ревьюером); **CI green — нет**, runner не зарегистрирован (#52) — это единственный незакрытый пункт, он трекается блокером #52 и будет закрыт первым же прогоном после регистрации runner'а. Закрываю issue: задача выполнена и влита, остаточная зависимость от #52 общая для всего Sprint 0 и не является дефектом #19. Refs #15, #52.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: embeddevio/awg-dash#19