Files
LMS/docs/evidence/baseline-2026-10-27.md
Vladislav Kopnin 1570196cc7 fix(import): построчные ошибки до записи
- preflight занятого логина и неактивной целевой группы
- applied=false с номером строки, без частичных студентов
- LDD-IMPORT-003; baseline 58 claims / 368 тестов
2026-10-29 19:20:16 +03:00

323 lines
20 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.

# 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
```
Результат: **368 тестов, 0 failures, 0 errors, 0 skipped**.
`ktlintCheck` и `detekt` — зелёные.
### 2.2. LDD gate
```bash
./gradlew evidenceCheck
```
Результат: **58/58 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-контур: **58/58 claims proven, P0 gaps = 0**,
**368 тестов**, 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. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.