# 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`, **44 утверждения**. | Критичность | Claims | Proven | Gaps | |---|---:|---:|---:| | P0 | 25 | 25 | 0 | | P1 | 21 | 21 | 0 | | **Итого** | **46** | **46** | **0** | Домены: - доступы: аноним/студент/преподаватель/админ, чужие курс/лаба/студент/группа, внутренние API identity; - повышение: dry-run, apply, идемпотентность, rollback, cancel; - жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход; - профиль студента и скачивание сдач; - материалы и активы, включая secure-by-default legacy-файлы; - миграции V11 и V13, полный upgrade с master-схемы; - аудит: токен, admin-only, сохранность после удаления, representative-хуки, пагинация; - аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия; - группы курса без N+1, авторство материалов, ведомость, UI/контракты, импорт, копирование лабораторных; - runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke. ## 2. Команды и результаты ### 2.1. Автоматические тесты ```bash ./gradlew test --rerun-tasks ``` Результат: **319 тестов, 0 failures, 0 errors, 0 skipped**. `ktlintCheck` и `detekt` — зелёные. ### 2.2. LDD gate ```bash ./gradlew evidenceCheck ``` Результат: **46/46 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–V11`, academic `V6–V12`, progress `V13`. Проверено: - `identity.audit_events` (`V10`) и `identity.refresh_tokens` (`V11`) созданы; - `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 job 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, вход админа и один доменный сценарий; это обязательный job `.gitlab-ci.yml` на каждом 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` и перечисляет несвязанные файлы для привязки или удаления; - связанный актив по-прежнему наследует видимость статьи → раздела → главы: публичная `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; отчисленный, выпущенный, архивный студент, архивный аккаунт и закрытая группа не могут обновить сессию; - web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION` (`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе); - access обновляется автоматически за 2 минуты до истечения, поэтому активная вкладка не требует входа каждые 15 минут; logout отзывает refresh. Прямой API-клиент должен сам использовать `POST /api/v1/auth/refresh`; автоматического обновления вне web BFF нет. Результат: **PASS**, claims `LDD-AUTH-001`, `LDD-AUTH-002`, `LDD-AUTH-003`. ### 2.7. Пагинация, N+1 и масштаб - `GET /api/v1/audit` принимает `page` и `limit` (максимум 500); в UI есть «Показать ещё». Это offset-пагинация, а не keyset. - `GET /api/v1/courses/{id}/groups` возвращает привязанные группы с именами одним вызовом; страница преподавателя не делает N+1 по именам групп. - Полный load использует seed на **100 групп / 1000 студентов** (SQL, только release-контур) и **125 параллельных чтений** курса, лаб, профиля, групп и аудита: **0 ошибок, p95 < 1 с** (порог p95 < 2 с). Остаются offset-пагинация аудита и N+1 в отдельных лабораторных списках/ведомости — это лабораторные детали, не продуктовые риски доступа. Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-PERF-001`, `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 на полном стеке, одноразовая БД: - admin: вход, `/admin/courses` с курсом, `/admin/audit` с событием; - teacher: вход, курс, ведомость (группа/студент, без админских кнопок), профиль студента, создание главы курса; - anonymous: `/admin/audit` → `/login`; - student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`. Результат: **4/4 passed**, evidence: `build/evidence/browser.json`. Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы. ### 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`. В `.gitlab-ci.yml`: - `check` — `test ktlintCheck detekt evidenceCheck` на push/MR; - `smoke` — `ldd-smoke.sh` (полный стек, health, вход админа, один курс) на каждом push/MR; - `release-evidence` — полный `ldd-release.sh` с Chromium/Node: обязателен для тегов, для веток запускается вручную. ### 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-токены ротируются и отзываются; закрытый статус блокирует продление сессии, а web-сессия переживает 15-минутный access-токен. - Аудит листается по страницам, а группы курса отдаются одним вызовом без N+1. - Аудит пишется и читается; upgrade не теряет синтетические данные. - Мутационная прочность критичных компонентов измерена, а не заявлена. - Масштабный 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 — один Chromium и 4 сценария.** Нет визуальной регрессии, Safari/Firefox и полного мобильного набора страниц. 4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные, без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия. 5. **Offset-пагинация аудита.** Вставки между страницами могут сдвигать выдачу; keyset-курсор не реализован. 6. **N+1 в отдельных лабораторных списках/ведомости.** Имена групп курса и аудит исправлены, но не все большие списки собраны пакетно. 7. **Нет абсолютного лимита web-сессии.** Sliding-сессия может продлеваться бесконечно при непрерывной активности; отзыв по статусу при этом работает. 8. **Полный release-evidence не на каждом коммите.** На push/MR идёт smoke, а mutation/browser/scale — для тегов или вручную. 9. **Внешняя интеграция «Контингент» не проверялась** — она остаётся заделом. 10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет; `01` и `1` — одно значение, это не открытый риск. ## 5. Вывод Текущая функциональность прошла LDD-контур: **46/46 claims proven, P0 gaps = 0**, **319 тестов**, upgrade, mutation baseline, browser E2E, smoke и масштабный load зелёные. Это достаточное основание продолжать доработки, но не «формальное доказательство всей системы». Следующий цикл LDD: 1. почистить инвентарь legacy-активов через `GET /api/v1/content/legacy-assets` (файлы уже не публичны, это гигиена хранилища); 2. добавить keyset-пагинацию аудита и убрать оставшийся N+1 в лабораторных списках/ведомости; 3. при необходимости ввести абсолютный лимит web-сессии; 4. расширить browser E2E на публикацию оценки и остальные мобильные страницы; 5. подготовить внешнюю интеграцию «Контингент»/`next.uniyar.ac.ru`.