# LDD baseline: 2026-10-27/28 Три LDD-цикла проверки текущей функциональности LMS; последний выполнен 2026-10-28 после закрытия продуктовых рисков. Добавлены реестр claims, gate `evidenceCheck`, runtime/migration/mutation/smoke-скрипты и браузерный E2E на Playwright. В ходе проверки найден и исправлен реальный дефект Jackson 3. ## 1. Что проверялось Реестр: `docs/evidence/claims.json`, **51 утверждение**. | Критичность | Claims | Proven | Gaps | |---|---:|---:|---:| | P0 | 25 | 25 | 0 | | P1 | 26 | 26 | 0 | | **Итого** | **51** | **51** | **0** | Домены: - доступы: аноним/студент/преподаватель/админ, чужие курс/лаба/студент/группа, внутренние API identity; - повышение: dry-run, apply, идемпотентность, rollback, cancel; - жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход; - профиль студента и скачивание сдач; - материалы и активы, включая secure-by-default legacy-файлы; - миграции V11–V13, полный upgrade с master-схемы; - аудит: токен, admin-only, сохранность после удаления, representative-хуки, keyset-пагинация; - аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия, абсолютный потолок 30 дней и очистка истёкших токенов; - группы курса и детали лабораторных без N+1, авторство материалов, ведомость, UI/контракты, импорт, копирование лабораторных; - runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke. ## 2. Команды и результаты ### 2.1. Автоматические тесты ```bash ./gradlew test --rerun-tasks ``` Результат: **346 тестов, 0 failures, 0 errors, 0 skipped**. `ktlintCheck` и `detekt` — зелёные. ### 2.2. LDD gate ```bash ./gradlew evidenceCheck ``` Результат: **51/51 claims proven, P0 gaps = 0**. Отчёт: `build/reports/evidence/index.md`. Gate устроен так: - каждый claim ссылается на конкретный JUnit-тест `Class#method`; - P0 без зелёного теста валит сборку; - P1-пробел показывается в отчёте; - runtime-проверки читаются из `build/evidence/*.json` и тоже должны быть `passed`, если claim на них ссылается. ### 2.3. Upgrade с master-схемы ```bash ./scripts/ldd-migration-upgrade.sh ``` Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic `V1–V5`, progress `V1–V9,V11,V12`, синтетические данные, затем ветка: identity `V4–V13`, academic `V6–V12`, progress `V13`. Проверено: - `identity.audit_events` (`V10`), `identity.refresh_tokens` (`V11`), `absolute_expires_at` (`V12`) и keyset-индекс `audit_events_occurred_id_idx` (`V13`) созданы; - `students`, `groups`, `membership_history` сохранились; - `labs.course_id`, `chapters.course_id`, `article_assets` созданы; - `grade_comments.author_account_id`, `lab_grades.updated_by_account_id` созданы; - оценки и комментарии сохранились; - повторное применение `V13` идемпотентно. Результат: **PASS**, evidence: `build/evidence/migration.json`. ### 2.4. Runtime-smoke полного стека и CI smoke ```bash ./scripts/ldd-runtime.sh ./scripts/ldd-smoke.sh # то же, что CI workflow smoke ``` Сценарий: одноразовая БД, `bootJar`, запуск identity/academic/progress/gateway/web, реальные HTTP-вызовы через gateway. Проверено: - health всех пяти сервисов = `UP`; - `adminExists=false` до setup, создание админа, `/auth/me` = `ADMIN`; - создание группы, курса, преподавателя, привязка группы, создание лабы; - преподаватель входит и видит свой курс, лабу и состав группы; - чужой преподаватель получает `403` на курс, лабу и аудит; - админ видит audit-события `COURSE_CREATE`, `TEACHER_CREATE`, `COURSE_TEACHER_ASSIGN`, `COURSE_GROUP_ATTACH`, `LAB_CREATE`; - web `/login` отдаёт `200`, публичное дерево материалов доступно, курсовые материалы анонимно — `401`. `ldd-smoke.sh` выполняет тот же полный стек, health, вход админа и один доменный сценарий; это обязательный workflow `.woodpecker/smoke.yaml` на каждом push/MR, поэтому базовый runtime-контур проверяется до merge, а не только перед релизом. Результат: **PASS**, evidence: `build/evidence/runtime.json` и `build/evidence/smoke.json`. ### 2.5. Legacy-активы: secure-by-default Проверено автоматическими тестами доступа к активам: - файл без строки в `article_assets` отдаёт `404` анониму, студенту и преподавателю (существование не раскрывается); - админ может скачать такой файл; - `GET /api/v1/content/legacy-assets` защищён `ROLE_ADMIN` и перечисляет несвязанные файлы; админская страница `/admin/legacy-assets` и `scripts/legacy-assets.sh` дают тот же список для привязки или удаления; - связанный актив по-прежнему наследует видимость статьи → раздела → главы: публичная `ALL` — без входа, курсовая — только участникам курса, скрытая для студента — `404`. Старые внешние ссылки на несвязанные файлы у обычных пользователей перестают работать. Это осознанное изменение поведения: безопасность важнее совместимости с «осиротевшими» активами. Результат: **PASS**, claims `LDD-ASSET-001`, `LDD-ASSET-002`. ### 2.6. Аутентификация, refresh и web-сессия - identity `V11__refresh_tokens.sql` хранит только SHA-256-хеш refresh-токена, срок, отзыв и цепочку замен; - access JWT — 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh — 7 дней (`LMS_REFRESH_TOKEN_TTL`); - `POST /api/v1/auth/refresh` ротирует пару; повторное использование старого токена отзывает все сессии аккаунта; - logout и смена пароля отзывают refresh; отчисленный, выпущенный, архивный студент, архивный аккаунт и закрытая группа не могут обновить сессию; - `V12__refresh_absolute.sql` задаёт `absolute_expires_at`; ротация не продлевает абсолютный срок, после 30 дней refresh отклоняется; - web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), но не дольше 30 дней (`LMS_SESSION_ABSOLUTE_TIMEOUT`); cookie `LMS_SESSION` (`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе и ограничен остатком абсолютного срока); - access обновляется автоматически за 2 минуты до истечения, поэтому активная вкладка не требует входа каждые 15 минут; logout отзывает refresh; - истёкшие refresh-токены старше retention (30 дней) удаляет ночная задача (`LMS_REFRESH_TOKEN_CLEANUP_CRON`), свежие отозванные токены сохраняются для detection reuse. Прямой API-клиент должен сам использовать `POST /api/v1/auth/refresh`; автоматического обновления вне web BFF нет. Результат: **PASS**, claims `LDD-AUTH-001`, `LDD-AUTH-002`, `LDD-AUTH-003`, `LDD-AUTH-006`, `LDD-AUTH-007`. ### 2.7. Пагинация, N+1 и масштаб - `GET /api/v1/audit` принимает keyset-курсор `cursor` (opaque `occurredAt|id`), `limit` (максимум 500) и `action`; offset `page` остался для совместимости. Курсорные страницы не пересекаются, битый курсор — `400`; в UI «Показать ещё» идёт по курсору, «В начало» возвращает к первой странице. - `GET /api/v1/labs/details` отдаёт все лабораторные курса с уровнями и вариантами одним запросом; progress делает ровно один HTTP-вызов вместо запроса на каждую лабораторную. - `GET /api/v1/courses/{id}/groups` возвращает привязанные группы с именами одним вызовом; страница преподавателя не делает N+1 по именам групп. - Полный load использует seed на **100 групп / 1000 студентов** (SQL, только release-контур) и **125 параллельных чтений** курса, лаб, профиля, групп и аудита: **0 ошибок, p95 < 1 с** (порог p95 < 2 с). Остаётся запрос сдач по выбранной лабораторной в ведомости — это не N+1 по студентам и не продуктовый риск доступа. Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-AUDIT-005`, `LDD-PERF-001`, `LDD-PERF-002`, `LDD-RUNTIME-003`. ### 2.8. Mutation testing ```bash ./scripts/ldd-mutation.sh ``` PIT запускается через CLI с JUnit 4 harness: JUnit5-плагин PIT несовместим с JUnit Platform 6 из Spring Boot 4. | Компонент | Мутаций | Убито | Test strength | Порог | |---|---:|---:|---:|---:| | `CourseAccessService` | 56 | 42 | **75%** | 70% | | `PromotionPlanner` | 162 | 109 | **69%** | 60% | Результат: **PASS**, evidence: `build/evidence/mutation.json`. Остаток — эквивалентные мутации, Kotlin-интринсики и ветки, не входящие в текущий P0-контур. ### 2.9. Браузерный E2E ```bash ./scripts/ldd-browser.sh ``` Playwright в Chromium, Firefox и WebKit на полном стеке, одноразовая БД: - admin: вход, `/admin/courses` с курсом, `/admin/audit` с событием; - teacher: вход, курс, ведомость (группа/студент, без админских кнопок), профиль студента, создание главы курса; - anonymous: `/admin/audit` → `/login`; - student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`; - admin: cursor-пагинация аудита («Показать ещё» → «Продолжение» → «В начало»); - admin: страница `/admin/legacy-assets` со списком или пустым состоянием. Результат: **18/18 passed** (6 сценариев × 3 браузера), evidence: `build/evidence/browser.json`. Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы. На Fedora WebKit запускается через Docker-образ Playwright, потому что системных Ubuntu-библиотек нет. ### 2.10. Конкурентные apply/rollback `PromotionConcurrencyIntegrationTest` (без `@Transactional`, два потока, advisory lock): два одновременных apply идемпотентны и пишут history один раз; два одновременных rollback дают ровно один успех и один `ConflictException`, состояние восстанавливается один раз. ### 2.11. Release gate и CI `./scripts/ldd-release.sh` последовательно запускает тесты/линтеры, upgrade, mutation, runtime + scale и browser E2E, затем финальный `evidenceCheck`. В `.woodpecker/` (Gitea + Woodpecker 3.x): - `check` — `test ktlintCheck detekt evidenceCheck` на push/MR; - `smoke` — `ldd-smoke.sh` (полный стек, health, вход админа, один курс) на каждом push/MR, после `check`; - `release` — полный `ldd-release.sh` с Chromium/Firefox/WebKit и Node: обязателен для тегов, для веток запускается вручную. - workflows требуют метку `isolation=dedicated-vm`; агент работает на отдельной VM, шаги — в `lms-ci:1` с nested dockerd и без хост-Docker сокета. - smoke/release начинаются с `scripts/ci-preflight.sh`, который проверяет Java 21, Docker Compose v2, Python 3, Node и Playwright. - Первый реальный прогон на Woodpecker-агенте ещё не подтверждён; локальные скрипты и конфигурация проверены, установка — `deploy/WOODPECKER.md`. ### 2.12. Найденный дефект Первый browser E2E упал на создании студента: API вернул `400`, потому что Jackson 3 не применял Kotlin-модуль, и DTO с default-значениями требовали все поля JSON. Исправлено: `jackson-module-kotlin` для Jackson 3 + `JsonMapperBuilderCustomizer` в identity/academic/progress/web. Добавлен регрессионный тест `TeacherIntegrationTest.student creation accepts omitted optional json fields`. ## 3. Что это доказывает - Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля, материалов, миграции, аудита и аутентификации имеют зелёные автоматические свидетельства. - Приложение собирается, мигрирует с master-схемы и поднимается полным стеком на чистой БД. - Ключевой доменный поток «админ → группа → курс → преподаватель → лаба» проходит через реальные HTTP/gateway/БД и браузер. - Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке. - Legacy-актив без привязки не утекает обычным пользователям, а админ может провести инвентаризацию. - Refresh-токены ротируются и отзываются; закрытый статус блокирует продление сессии, абсолютный потолок 30 дней ограничивает сессию, а web-сессия переживает 15-минутный access-токен. - Аудит листается keyset-курсором, детали лабораторных и группы курса отдаются пакетно без N+1. - Аудит пишется и читается; upgrade не теряет синтетические данные; истёкшие refresh-токены очищаются. - Мутационная прочность критичных компонентов измерена, а не заявлена. - Масштабный load на 100 группах / 1000 студентов проходит без ошибок. - Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом. ## 4. Чего это не доказывает 1. **Mutation testing не 100%.** Остаются эквивалентные мутанты и Kotlin/Spring-специфичные мутации; PIT harness — JUnit 4, а не JUnit 5. 2. **Property-based тесты покрывают не все ветки.** Есть генераторы для матрицы доступа, планировщика и 10 транзакционных apply/rollback-кейсов, но не для всех сочетаний полей и распределённой нагрузки. 3. **Браузерный E2E — 3 браузера, 6 сценариев.** Нет визуальной регрессии и полного мобильного набора страниц; WebKit на Fedora требует Docker-образа. 4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные, без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия. 5. **Сдачи по выбранной лабораторной в ведомости** запрашиваются отдельно; это не N+1 по студентам, но и не пакетная выборка. 6. **Инвентаризация legacy-активов на production не выполнена.** UI, API и скрипт готовы, но реальные файлы нужно привязать или удалить вручную. 7. **Первый CI-прогон не подтверждён.** Конфигурация `.woodpecker/` и preflight готовы, но Woodpecker-сервер и Docker-backend агент на отдельной VM ещё нужно развернуть и проверить на первом push-MR и теге. 8. **Полный release не на каждом коммите.** На push/MR идёт smoke, а mutation/browser/scale — для тегов или вручную. 9. **Интеграция «Контингент» исключена из плана** — без нового ТЗ и доступа к API она не проверялась и не реализуется. 10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет; `01` и `1` — одно значение, это не открытый риск. ## 5. Вывод Текущая функциональность прошла LDD-контур: **51/51 claims proven, P0 gaps = 0**, **346 тестов**, upgrade, mutation baseline, 18/18 browser E2E, smoke и масштабный load зелёные. Это достаточное основание продолжать доработки, но не «формальное доказательство всей системы». Следующий цикл LDD: 1. прогнать инвентаризацию legacy-активов на production/staging через `/admin/legacy-assets` или `scripts/legacy-assets.sh` и привязать/удалить найденные файлы; 2. развернуть Woodpecker и подтвердить первый push-MR pipeline и tag-job; 3. при необходимости расширить E2E на визуальную регрессию и остальные мобильные страницы; 4. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.