feat: закрыть остаточный backlog

- keyset-пагинация аудита с курсором (occurred_at, id) и UI «Показать ещё»
- пакетный /api/v1/labs/details и переход progress на один запрос
- абсолютный потолок сессии 30 дней (identity V12 + web), очистка refresh
- админская инвентаризация legacy-активов (/admin/legacy-assets, скрипт)
- E2E в Chromium/Firefox/WebKit (18/18), CI preflight
- LDD: 51 claim, 346 тестов, docs и handoff обновлены
This commit is contained in:
2026-10-28 19:29:23 +03:00
parent d45137220c
commit 1e1e9ae0e4
64 changed files with 2124 additions and 372 deletions

View File

@@ -7,13 +7,13 @@ Playwright. В ходе проверки найден и исправлен ре
## 1. Что проверялось
Реестр: `docs/evidence/claims.json`, **44 утверждения**.
Реестр: `docs/evidence/claims.json`, **51 утверждение**.
| Критичность | Claims | Proven | Gaps |
|---|---:|---:|---:|
| P0 | 25 | 25 | 0 |
| P1 | 21 | 21 | 0 |
| **Итого** | **46** | **46** | **0** |
| P1 | 26 | 26 | 0 |
| **Итого** | **51** | **51** | **0** |
Домены:
@@ -23,12 +23,13 @@ Playwright. В ходе проверки найден и исправлен ре
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
- профиль студента и скачивание сдач;
- материалы и активы, включая secure-by-default legacy-файлы;
- миграции V11 и V13, полный upgrade с master-схемы;
- миграции V11–V13, полный upgrade с master-схемы;
- аудит: токен, admin-only, сохранность после удаления, representative-хуки,
пагинация;
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия;
- группы курса без N+1, авторство материалов, ведомость, UI/контракты, импорт,
копирование лабораторных;
keyset-пагинация;
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия,
абсолютный потолок 30 дней и очистка истёкших токенов;
- группы курса и детали лабораторных без N+1, авторство материалов, ведомость,
UI/контракты, импорт, копирование лабораторных;
- runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke.
## 2. Команды и результаты
@@ -39,7 +40,7 @@ Playwright. В ходе проверки найден и исправлен ре
./gradlew test --rerun-tasks
```
Результат: **319 тестов, 0 failures, 0 errors, 0 skipped**.
Результат: **346 тестов, 0 failures, 0 errors, 0 skipped**.
`ktlintCheck` и `detekt` — зелёные.
### 2.2. LDD gate
@@ -48,7 +49,7 @@ Playwright. В ходе проверки найден и исправлен ре
./gradlew evidenceCheck
```
Результат: **46/46 claims proven, P0 gaps = 0**.
Результат: **51/51 claims proven, P0 gaps = 0**.
Отчёт: `build/reports/evidence/index.md`.
Gate устроен так:
@@ -67,11 +68,13 @@ Gate устроен так:
Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic
`V1–V5`, progress `V1–V9,V11,V12`, синтетические данные, затем ветка:
identity `V4–V11`, academic `V6–V12`, progress `V13`.
identity `V4–V13`, academic `V6–V12`, progress `V13`.
Проверено:
- `identity.audit_events` (`V10`) и `identity.refresh_tokens` (`V11`) созданы;
- `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` созданы;
@@ -117,7 +120,8 @@ identity `V4–V11`, academic `V6–V12`, progress `V13`.
преподавателю (существование не раскрывается);
- админ может скачать такой файл;
- `GET /api/v1/content/legacy-assets` защищён `ROLE_ADMIN` и перечисляет
несвязанные файлы для привязки или удаления;
несвязанные файлы; админская страница `/admin/legacy-assets` и
`scripts/legacy-assets.sh` дают тот же список для привязки или удаления;
- связанный актив по-прежнему наследует видимость статьи → раздела → главы:
публичная `ALL` — без входа, курсовая — только участникам курса, скрытая
для студента — `404`.
@@ -138,30 +142,44 @@ identity `V4–V11`, academic `V6–V12`, progress `V13`.
токена отзывает все сессии аккаунта;
- logout и смена пароля отзывают refresh; отчисленный, выпущенный, архивный
студент, архивный аккаунт и закрытая группа не могут обновить сессию;
- web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе);
- `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.
вкладка не требует входа каждые 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`.
Результат: **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` принимает `page` и `limit` (максимум 500); в UI есть
«Показать ещё». Это offset-пагинация, а не keyset.
- `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 с).
Остаются offset-пагинация аудита и N+1 в отдельных лабораторных
списках/ведомости — это лабораторные детали, не продуктовые риски доступа.
Остаётся запрос сдач по выбранной лабораторной в ведомости — это не N+1 по
студентам и не продуктовый риск доступа.
Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-PERF-001`, `LDD-RUNTIME-003`.
Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-AUDIT-005`, `LDD-PERF-001`,
`LDD-PERF-002`, `LDD-RUNTIME-003`.
### 2.8. Mutation testing
@@ -187,16 +205,20 @@ PIT запускается через CLI с JUnit 4 harness: JUnit5-плаги
./scripts/ldd-browser.sh
```
Playwright/Chromium на полном стеке, одноразовая БД:
Playwright в Chromium, Firefox и WebKit на полном стеке, одноразовая БД:
- admin: вход, `/admin/courses` с курсом, `/admin/audit` с событием;
- teacher: вход, курс, ведомость (группа/студент, без админских кнопок),
профиль студента, создание главы курса;
- anonymous: `/admin/audit` → `/login`;
- student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`.
- student: вход, `/admin/audit` → `/student`, `/learn/course/{no}`;
- admin: cursor-пагинация аудита («Показать ещё» → «Продолжение» → «В начало»);
- admin: страница `/admin/legacy-assets` со списком или пустым состоянием.
Результат: **4/4 passed**, evidence: `build/evidence/browser.json`.
Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы.
Результат: **18/18 passed** (6 сценариев × 3 браузера), evidence:
`build/evidence/browser.json`. Дополнительно проверены сдача файла студентом и
mobile-размер teacher-страницы. На Fedora WebKit запускается через Docker-образ
Playwright, потому что системных Ubuntu-библиотек нет.
### 2.10. Конкурентные apply/rollback
@@ -215,8 +237,12 @@ mutation, runtime + scale и browser E2E, затем финальный `evidenc
- `check` — `test ktlintCheck detekt evidenceCheck` на push/MR;
- `smoke` — `ldd-smoke.sh` (полный стек, health, вход админа, один курс)
на каждом push/MR;
- `release-evidence` — полный `ldd-release.sh` с Chromium/Node: обязателен для
тегов, для веток запускается вручную.
- `release-evidence` — полный `ldd-release.sh` с Chromium/Firefox/WebKit и
Node: обязателен для тегов, для веток запускается вручную.
- smoke/release начинаются с `scripts/ci-preflight.sh`, который проверяет
Java 21, Docker Compose v2, Python 3, Node и Playwright.
- Первый реальный прогон на GitLab shell-runners ещё не подтверждён; локальные
скрипты и конфигурация проверены.
### 2.12. Найденный дефект
@@ -240,9 +266,12 @@ optional json fields`.
- Legacy-актив без привязки не утекает обычным пользователям, а админ может
провести инвентаризацию.
- Refresh-токены ротируются и отзываются; закрытый статус блокирует продление
сессии, а web-сессия переживает 15-минутный access-токен.
- Аудит листается по страницам, а группы курса отдаются одним вызовом без N+1.
- Аудит пишется и читается; upgrade не теряет синтетические данные.
сессии, абсолютный потолок 30 дней ограничивает сессию, а web-сессия
переживает 15-минутный access-токен.
- Аудит листается keyset-курсором, детали лабораторных и группы курса
отдаются пакетно без N+1.
- Аудит пишется и читается; upgrade не теряет синтетические данные;
истёкшие refresh-токены очищаются.
- Мутационная прочность критичных компонентов измерена, а не заявлена.
- Масштабный load на 100 группах / 1000 студентов проходит без ошибок.
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
@@ -254,35 +283,36 @@ optional json fields`.
2. **Property-based тесты покрывают не все ветки.** Есть генераторы для
матрицы доступа, планировщика и 10 транзакционных apply/rollback-кейсов,
но не для всех сочетаний полей и распределённой нагрузки.
3. **Браузерный E2E — один Chromium и 4 сценария.** Нет визуальной регрессии,
Safari/Firefox и полного мобильного набора страниц.
3. **Браузерный E2E — 3 браузера, 6 сценариев.** Нет визуальной регрессии и
полного мобильного набора страниц; WebKit на Fedora требует Docker-образа.
4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные,
без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
5. **Offset-пагинация аудита.** Вставки между страницами могут сдвигать
выдачу; keyset-курсор не реализован.
6. **N+1 в отдельных лабораторных списках/ведомости.** Имена групп курса и
аудит исправлены, но не все большие списки собраны пакетно.
7. **Нет абсолютного лимита web-сессии.** Sliding-сессия может продлеваться
бесконечно при непрерывной активности; отзыв по статусу при этом работает.
5. **Сдачи по выбранной лабораторной в ведомости** запрашиваются отдельно;
это не N+1 по студентам, но и не пакетная выборка.
6. **Инвентаризация legacy-активов на production не выполнена.** UI, API и
скрипт готовы, но реальные файлы нужно привязать или удалить вручную.
7. **Первый CI-прогон не подтверждён.** Конфигурация и preflight готовы, но
наличие shell-runners `lms-shell`/`lms-release` проверяется в GitLab.
8. **Полный release-evidence не на каждом коммите.** На push/MR идёт smoke,
а mutation/browser/scale — для тегов или вручную.
9. **Внешняя интеграция «Контингент» не проверялась** — она остаётся заделом.
9. **Интеграция «Контингент» исключена из плана** — без нового ТЗ и доступа к
API она не проверялась и не реализуется.
10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет;
`01` и `1` — одно значение, это не открытый риск.
## 5. Вывод
Текущая функциональность прошла LDD-контур: **46/46 claims proven, P0 gaps = 0**,
**319 тестов**, upgrade, mutation baseline, browser E2E, smoke и масштабный load
зелёные. Это достаточное основание продолжать доработки, но не «формальное
доказательство всей системы».
Текущая функциональность прошла LDD-контур: **51/51 claims proven, P0 gaps = 0**,
**346 тестов**, upgrade, mutation baseline, 18/18 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`.
1. прогнать инвентаризацию legacy-активов на production/staging через
`/admin/legacy-assets` или `scripts/legacy-assets.sh` и привязать/удалить
найденные файлы;
2. подтвердить первый реальный CI-прогон на shell-runners и tag-job;
3. при необходимости расширить E2E на визуальную регрессию и остальные
мобильные страницы;
4. пакетно отдавать сдачи в ведомости, если появятся медленные курсы.