Sprint 0: OpenAPI 3.1 контракт API v1 + генерация TypeScript-клиента #19
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Owner: backend-engineer · Est: 3 SP · Priority: P0 · Sprint: 0 · Deps: —
Refs #15
Что сделать
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.ui/и тест «схема ↔ реализация» (контрактный тест в CI).Acceptance criteria
ui/src/api/generated/.PrivateKey/PresharedKeyв схеме, WHEN смотрю спецификацию, THEN поляwriteOnly,readOnly-ответов с ключами нет.DoD
Тесты на новый код ≥ 70 %, CI green, PR с ревью владельца роли.
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 — возобновление работы (28.10, 20:5x).
Провайдер восстановлен: ключ polza был деактивирован (
HTTP 403: API ключ заблокирован или деактивирован), воркеры выходилиrc=78(terminal provider error) — после ротации ключа владельцем всё работает, дневной лимит 400 ₽.Состояние работы по #19 на момент паузы: в ветке
feat/19-openapi(HEAD95f1337fd4f33550586a0cbd87214c23f1a808b7) уже лежит полный контракт и код (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'а.Отчёт 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}, закрытый enumErrorCode; любой не-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 — контракт остаётся источником истины.ui/src/api/generated/schema.d.ts(39 455 B) сгенерированopenapi-typescript@7.13.0и закоммичен..gitea/workflows/contract-openapi.yml(jobcontract— ruff/mypy/pytest; jobts-client— перегенерация +git diff --exit-code).Гейты — локальный прогон (Gitea Actions runner недоступен, #52)
AC
1. Генератор TS-клиента отрабатывает без ошибок — ВЫПОЛНЕНО
2. Отклонение реализации/схемы роняет контрактный тест — ВЫПОЛНЕНО (мутация → прогон → откат, blob после отката совпал)
/api/v1/statusотдаёт поле, которого нет в схемеtest_success_response_matches_spec[get-/api/v1/status-None]private_keyв спеке убранwriteOnlytest_secret_fields_are_write_only,test_write_only_secrets_removed_from_response_schema,test_private_key_in_response_fails_validationprivate_keyутёк в ответ/api/v1/configtest_responses_contain_no_secret_material+ валидация схемы3. В ответах нет приватных ключей — ВЫПОЛНЕНО —
test_responses_contain_no_secret_materialпроверяет 7 ответов (/configGET+PUT,/status,/devices,/logs,/auth/session,/auth/logout) на отсутствие и сентинел-значений, и имён полей; схема ответа после вырезания writeOnlyprivate_key/preshared_keyне содержит.DoD
Важно для ревью
pyproject.toml/requirements-dev.txtприходят из #16 (PR #47 не влит) — до влития #47 jobcontractне стартует; файлы PR и #47 не пересекаются.1555a68…,4c6ba6e…), чтобы проверялся ровно тот код, что гонялся локально.MockState— временное состояние до реального контрол-плейна Sprint 1+.Прошлые run по этой задаче
Run 1–2 упали по контексту, run 3 — 403 от провайдера до создания PR, run 4 — инфраструктурный spawn_failed. Не переделывал работу с нуля: проверил дерево ветки по blob SHA, досинхронизировал 2 тест-файла и коммит сгенерированного клиента, прогнал все гейты и негативные проверки.
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.