docs(qa): tester guide for versioning and AI features
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,301 @@
|
|||||||
|
# Инструкция для тестировщика: версионирование тестов и AI-функции
|
||||||
|
|
||||||
|
> Контур: новый Flask на `:3108` (после E1.0–E1.3, E1.8). Старый Node — на `:3107`,
|
||||||
|
> для проверки этих задач **используйте только `:3108`**.
|
||||||
|
|
||||||
|
Перед началом:
|
||||||
|
|
||||||
|
1. Откройте `http://<host>:3108/login` под учёткой автора (роль `manager` или
|
||||||
|
`admin`). Если сидов нет — заведите пользователя в `clinic_tests.users`
|
||||||
|
обычным способом.
|
||||||
|
2. Для AI-задач: откройте `http://<host>:3108/settings` и убедитесь, что
|
||||||
|
статус ключа = «Задан» и кнопка **«Проверить подключение»** возвращает
|
||||||
|
зелёный **OK · provider/model · NN мс**. Если ключ не задан — AI-задачи
|
||||||
|
нужно прогнать в негативном сценарии (см. блок A.0).
|
||||||
|
3. Откройте `http://<host>:3108/tests` — это каталог.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 1. Версионирование при правке после попыток
|
||||||
|
|
||||||
|
### Что должно работать
|
||||||
|
|
||||||
|
| Правило | Поведение |
|
||||||
|
|---|---|
|
||||||
|
| Нет попыток | Автор правит тест **на месте**, номер версии не меняется. |
|
||||||
|
| Есть ≥ 1 попытка | Любое сохранение изменений **создаёт новую версию** (`version + 1`), старая становится неактивной, но **сохраняется в БД** и связана через `parent_id`. |
|
||||||
|
| Цепочка | Все версии связаны (parent → child), на странице «Версии» видны все. |
|
||||||
|
| Каталог | В списке видна **только активная** версия цепочки. |
|
||||||
|
| Переключение активной версии | Автор может вручную сделать активной любую версию — остальные автоматически становятся неактивными. |
|
||||||
|
| Деактивация цепочки | Тест можно скрыть целиком; данные не удаляются. |
|
||||||
|
| Корректность истории | Каждая попытка привязана к **той версии**, по которой её проходили — разбор ошибок остаётся корректным после правок. |
|
||||||
|
|
||||||
|
### Сценарий 1.1. Правка теста до попыток (версия не растёт)
|
||||||
|
|
||||||
|
1. В каталоге → **«Создать тест»**, заполните название и описание → создать.
|
||||||
|
2. В редакторе добавьте 2–3 вопроса по 3–4 варианта, сохраните.
|
||||||
|
3. Откройте этот же тест в редакторе ещё раз, измените:
|
||||||
|
- название;
|
||||||
|
- описание;
|
||||||
|
- текст одного вопроса;
|
||||||
|
- пометьте один вариант как правильный иначе;
|
||||||
|
- удалите/добавьте вариант.
|
||||||
|
4. **«Сохранить»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Сообщение «Сохранено.» (без слов «создана новая версия»).
|
||||||
|
- В БД: `SELECT version FROM test_versions WHERE test_id = '<id>'` → **одна** строка с `version = 1, is_active = true`.
|
||||||
|
- Эндпоинт `GET /api/tests/<id>/versions` → массив из 1 элемента, `hasAttempts: false`.
|
||||||
|
|
||||||
|
### Сценарий 1.2. Появление первой попытки → форк новой версии
|
||||||
|
|
||||||
|
> Прохождение теста (UI) пока не реализовано в Flask-контуре (запланировано
|
||||||
|
> в **E1.4**). Поэтому факт попытки имитируется напрямую в БД — это норма
|
||||||
|
> для текущего этапа.
|
||||||
|
|
||||||
|
1. Возьмите `id` теста и `id` активной версии:
|
||||||
|
```sql
|
||||||
|
SELECT t.id AS test_id, tv.id AS version_id
|
||||||
|
FROM tests t JOIN test_versions tv ON tv.test_id = t.id AND tv.is_active
|
||||||
|
WHERE t.title = 'Ваш тест';
|
||||||
|
```
|
||||||
|
2. Создайте «попытку» (минимальный INSERT, любой пользователь, любое
|
||||||
|
состояние; нам нужен только сам факт записи в `test_attempts`):
|
||||||
|
```sql
|
||||||
|
INSERT INTO test_attempts (id, test_version_id, user_id, status, created_at)
|
||||||
|
VALUES (gen_random_uuid(), '<version_id>', '<any_user_id>', 'completed', now());
|
||||||
|
```
|
||||||
|
3. В UI откройте редактор того же теста, **измените хотя бы один вопрос**
|
||||||
|
(текст / правильность варианта / число вариантов) и **«Сохранить»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Сообщение «Сохранено (создана новая версия — есть попытки прохождения).»
|
||||||
|
- В БД: `SELECT version, is_active, parent_id FROM test_versions WHERE test_id=...`
|
||||||
|
→ **две** строки:
|
||||||
|
- `version = 1, is_active = false, parent_id = NULL` (старая, не удалена);
|
||||||
|
- `version = 2, is_active = true, parent_id = <id версии 1>` (новая).
|
||||||
|
- Старая попытка по-прежнему ссылается на `version_id` из v1, и её ответы/вопросы остаются те же — разбор ошибок не «съехал».
|
||||||
|
|
||||||
|
### Сценарий 1.3. Правка только метаданных после попыток (без форка)
|
||||||
|
|
||||||
|
После сценария 1.2:
|
||||||
|
|
||||||
|
1. В редакторе **не трогайте** вопросы и варианты. Поменяйте только название
|
||||||
|
или описание или проходной балл. Сохраните.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Сообщение «Сохранено.» (без форка).
|
||||||
|
- `version` не вырос; метаданные обновились на активной версии.
|
||||||
|
|
||||||
|
> Логика: форк делается только если после попыток меняется **содержание**
|
||||||
|
> (вопросы/варианты). Чисто косметические правки шапки версию не плодят.
|
||||||
|
|
||||||
|
### Сценарий 1.4. Каталог показывает только активную версию
|
||||||
|
|
||||||
|
1. После 1.2 откройте `/tests` под автором и под не-автором (если есть назначения).
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- В каталоге — одна карточка теста с пометкой `v.2` (активная). Версия 1 в каталог не попадает, но видна автору на странице «Версии» теста.
|
||||||
|
|
||||||
|
### Сценарий 1.5. Ручное переключение активной версии
|
||||||
|
|
||||||
|
1. Получите id v1 и v2: `SELECT id, version, is_active FROM test_versions WHERE test_id=...`
|
||||||
|
2. Сделайте активной v1:
|
||||||
|
```bash
|
||||||
|
curl -X POST -b cookies.txt \
|
||||||
|
http://<host>:3108/api/tests/<test_id>/versions/<v1_id>/activate
|
||||||
|
```
|
||||||
|
(cookie сессии берёте из браузера или логином через `/api/auth/login`).
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Ответ `{ ok: true, activeVersionId: "<v1_id>" }`.
|
||||||
|
- В БД: `is_active = true` только у v1, у v2 — `false`.
|
||||||
|
- В каталоге карточка теста снова показывает `v.1`.
|
||||||
|
|
||||||
|
### Сценарий 1.6. Деактивация цепочки целиком
|
||||||
|
|
||||||
|
1. В редакторе снимите чекбокс **«Цепочка активна»** и сохраните.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- В каталоге `/tests` теста больше не видно (ни в visible, ни у не-авторов).
|
||||||
|
- У автора он появляется в блоке **«Скрытые вами»** (внизу каталога).
|
||||||
|
- В БД: `tests.is_active = false`, версии и попытки нетронуты.
|
||||||
|
- Включение чекбокса обратно возвращает тест в каталог.
|
||||||
|
|
||||||
|
### Сценарий 1.7. Корректность истории по старым попыткам
|
||||||
|
|
||||||
|
> Полноценный разбор пользовательских ответов появится в **E1.4** вместе
|
||||||
|
> с UI прохождения. Сейчас минимально проверяем, что данные старой версии
|
||||||
|
> не повреждены.
|
||||||
|
|
||||||
|
1. После 1.2 в БД:
|
||||||
|
```sql
|
||||||
|
SELECT q.text
|
||||||
|
FROM questions q
|
||||||
|
JOIN test_versions tv ON tv.id = q.test_version_id
|
||||||
|
WHERE tv.test_id = '<test_id>' AND tv.version = 1;
|
||||||
|
```
|
||||||
|
2. **Ожидается:** видны вопросы **в том виде, в каком они были до правки**
|
||||||
|
(а не текущая версия v2). Эта же выборка должна совпадать с
|
||||||
|
`q.test_version_id` любой попытки, которую вы создали в 1.2.
|
||||||
|
|
||||||
|
### Что фиксировать как баг
|
||||||
|
|
||||||
|
- После правки **с попытками** в БД остался один `test_versions`-ряд (нет форка).
|
||||||
|
- После правки **без попыток** появилась `version = 2` (лишний форк).
|
||||||
|
- При активации одной версии другие не сбросились в `is_active = false`.
|
||||||
|
- Каталог показывает неактивную версию или скрытый тест.
|
||||||
|
- Сообщение в UI после сохранения не совпадает с реальным поведением (форк есть, текст «Сохранено.» без уточнения, или наоборот).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часть 2. AI-функции (E1.2 + E1.8)
|
||||||
|
|
||||||
|
### Заметка о ключе
|
||||||
|
|
||||||
|
Изначально в ТЗ предполагалось хранить ключ в БД и вводить на `/settings`.
|
||||||
|
По согласованию ключ **общий** и хранится в `ENV` контейнера
|
||||||
|
(`DEEPSEEK_API_KEY` / `OPENAI_API_KEY`). Страница `/settings` остаётся,
|
||||||
|
но в ней — только статус и кнопка проверки подключения. Поле ввода ключа в UI **не нужно** (это не баг).
|
||||||
|
|
||||||
|
### A.0. Негативный кейс — ключ не задан
|
||||||
|
|
||||||
|
1. На сервере уберите `DEEPSEEK_API_KEY` из окружения и перезапустите контейнер.
|
||||||
|
2. Откройте `/settings`.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Бейдж «Не задан» (красный).
|
||||||
|
- Блок «Как задать ключ» с примером `.env`.
|
||||||
|
- Кнопка **«Проверить подключение»** возвращает красный блок с текстом про незаданный ключ.
|
||||||
|
- В редакторе при нажатии **«Сгенерировать по сетке» / «по названию» / «Проверить тест» / «Улучшить тест» / «AI: вопрос»** появляется confirm:
|
||||||
|
«… Открыть Настройки?» → согласие открывает `/settings`.
|
||||||
|
|
||||||
|
После проверки верните ключ и `docker compose ... up -d` — переходим к позитивным сценариям.
|
||||||
|
|
||||||
|
### A.1. `/settings` → «Проверить подключение»
|
||||||
|
|
||||||
|
1. На `/settings` нажмите **«Проверить подключение»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- В течение нескольких секунд — зелёный блок: `OK · <provider> / <model> · <ms> мс` и сэмпл ответа.
|
||||||
|
- Provider/Model совпадают с ENV (`deepseek` + `deepseek-chat` по умолчанию).
|
||||||
|
|
||||||
|
### A.2. «Сгенерировать тест по названию» (E1.8)
|
||||||
|
|
||||||
|
1. Создайте новый пустой тест (никаких вопросов).
|
||||||
|
2. В редакторе нажмите **«Сгенерировать по названию»**.
|
||||||
|
3. На запрос «Сколько вопросов?» введите, например, `8`; «Сколько вариантов?» — `4`.
|
||||||
|
4. Дождитесь готовности → confirm «Применить как черновик?».
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Кнопка **активна только** когда поле «Название» заполнено. Если очистить название — нажатие даёт алерт «Сначала заполните название теста.» и фокус возвращается в название.
|
||||||
|
- Появляется ровно ~8 вопросов по ~4 варианта (модель может слегка отклониться по инструкции, это допустимо).
|
||||||
|
- Тексты — на русском, по теме названия.
|
||||||
|
- В каждом вопросе хотя бы один вариант помечен как правильный.
|
||||||
|
- Отказ в confirm не меняет редактор; согласие — заменяет.
|
||||||
|
- Сохранение работает, в БД появляется версия с этими вопросами.
|
||||||
|
|
||||||
|
### A.3. «Сгенерировать тест по сетке» (E1.2 — было)
|
||||||
|
|
||||||
|
1. Откройте тест, в котором уже руками настроены 5 вопросов, по 3 варианта в каждом, 2 из вопросов помечены как «Несколько правильных».
|
||||||
|
2. Нажмите **«Сгенерировать по сетке»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Возвращается ровно 5 вопросов.
|
||||||
|
- В тех же позициях, что у вас стояли «Несколько правильных», — у новых вопросов несколько правильных вариантов.
|
||||||
|
- Число вариантов в каждом вопросе совпадает.
|
||||||
|
- При несовпадении сетки эндпоинт вернул бы 502 с кодом `llm_shape` (модель «не попала») — допустимая редкая ошибка, повторите.
|
||||||
|
|
||||||
|
### A.4. «Проверить тест» (E1.8)
|
||||||
|
|
||||||
|
1. Создайте тест с парой нарочно слабых мест:
|
||||||
|
- один вопрос с длинной мутной формулировкой;
|
||||||
|
- один вопрос, где все варианты слишком похожи или в качестве «дистракторов» — очевидная ерунда.
|
||||||
|
2. Нажмите **«Проверить тест»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Открывается модалка «Проверка теста».
|
||||||
|
- Есть цветная плашка с одним из вердиктов: **Годен / Есть замечания / Серьёзные проблемы**, и краткое резюме (1–2 предложения).
|
||||||
|
- Ниже — список разделов («Чёткость формулировок», «Качество дистракторов», «Охват темы», «Сбалансированность сложности»). Разделы без замечаний пропускаются.
|
||||||
|
- В списке — конкретные пункты на русском, по делу.
|
||||||
|
- Закрытие модалки крестиком или кнопкой «Закрыть» работает.
|
||||||
|
|
||||||
|
### A.5. «Улучшить тест» (E1.8) — массовое было → стало
|
||||||
|
|
||||||
|
1. Возьмите тест из A.4 (с ≥ 3 вопросами).
|
||||||
|
2. Нажмите **«Улучшить тест»**.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Открывается модалка с заголовком «Улучшение теста» и подсказкой «Отмечено N из M».
|
||||||
|
- Каждый изменённый вопрос — отдельная карточка:
|
||||||
|
- чекбокс «Вопрос #N» (по умолчанию **отмечен**);
|
||||||
|
- две колонки **Было** / **Стало**;
|
||||||
|
- изменённый текст в «Было» зачёркнут, в «Стало» — выделен;
|
||||||
|
- правильные варианты помечены ✓.
|
||||||
|
- Снимите галку с одного-двух вопросов и нажмите **«Применить выбранное»**.
|
||||||
|
- В редакторе **только** отмеченные вопросы заменены на улучшенные; остальные остались как были.
|
||||||
|
- Появляется надпись «Изменения применены. Не забудьте сохранить.» — нажмите **«Сохранить»** и проверьте версионирование (см. Часть 1).
|
||||||
|
- **Сетка не меняется**: число вопросов, число вариантов в каждом и значение «Несколько правильных» совпадают с исходными. Если модель «слетела» — эндпоинт возвращает 502 с `llm_shape` и UI показывает алерт; это не баг логики.
|
||||||
|
|
||||||
|
### A.6. AI-кнопка на конкретном вопросе (E1.2)
|
||||||
|
|
||||||
|
Сценарий «новый вопрос»:
|
||||||
|
|
||||||
|
1. Добавьте вопрос, **поле текста оставьте пустым**, число вариантов = 4, «Несколько правильных» — выкл.
|
||||||
|
2. Нажмите **«AI: вопрос/переформулировать»** на этом вопросе.
|
||||||
|
|
||||||
|
**Ожидается:** заполнен текст вопроса и все 4 варианта; ровно один помечен правильным; внизу — статус «AI: вопрос сгенерирован.»
|
||||||
|
|
||||||
|
Сценарий «переформулировать»:
|
||||||
|
|
||||||
|
1. Возьмите готовый вопрос с заполненным текстом.
|
||||||
|
2. Нажмите ту же кнопку.
|
||||||
|
|
||||||
|
**Ожидается:** меняется **только текст** вопроса (вариант ответа и правильность не трогаются), статус «AI: формулировка обновлена.»
|
||||||
|
|
||||||
|
### A.7. Импорт документа (E1.3)
|
||||||
|
|
||||||
|
1. Подготовьте файл `sample.pdf` или `sample.docx` со связным текстом (1–3 страницы) на русском.
|
||||||
|
2. В редакторе → AI-панель → **«Импорт документа»** → выберите файл.
|
||||||
|
|
||||||
|
**Ожидается:**
|
||||||
|
- Прогресс «Загружаем «sample.pdf»…».
|
||||||
|
- Confirm «Сгенерировано: «<title>», вопросов: N. Применить как новый черновик?» → согласие заменяет вопросы, отказ ничего не меняет.
|
||||||
|
- При файле > 16 МБ — ошибка от Flask (413/500), это норма (лимит).
|
||||||
|
- При файле неподдерживаемого формата (`.xlsx`, `.png`) — алерт «Неподдерживаемый формат…».
|
||||||
|
- При **отсутствующем** ключе AI: вместо confirm — алерт с текстом «Автогенерация выключена…» и первыми 600 символами извлечённого текста; вопросы **не** заменяются.
|
||||||
|
|
||||||
|
### A.8. Единая ошибка при отсутствии ключа
|
||||||
|
|
||||||
|
1. Уберите ключ (как в A.0). Откройте редактор любого теста.
|
||||||
|
2. По очереди нажимайте **«Сгенерировать по сетке»**, **«по названию»**, **«Проверить тест»**, **«Улучшить тест»**, **«AI: вопрос»**.
|
||||||
|
|
||||||
|
**Ожидается:** в каждом случае confirm с предложением открыть **/settings**, не молчаливый алерт. На бэке — JSON `{ error, code, settingsUrl: "/settings" }`, статус 502.
|
||||||
|
|
||||||
|
### Что фиксировать как баг (AI)
|
||||||
|
|
||||||
|
- Кнопка **«Сгенерировать по названию»** позволяет жать без названия и не показывает алерт.
|
||||||
|
- Модалка **«Проверить тест»** пуста или содержит англ. текст.
|
||||||
|
- В **«Улучшить тест»** меняется число вопросов / число вариантов / «Несколько правильных», т.е. сетка слетела, и UI всё равно применил.
|
||||||
|
- В **«AI: вопрос»** на пустом вопросе варианты не сгенерированы; на заполненном — варианты заменены без ведома пользователя.
|
||||||
|
- Любая AI-ошибка показывается без ссылки на `/settings`, когда ключ действительно отсутствует.
|
||||||
|
- Импорт документа подменяет вопросы **без confirm**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шпаргалка: что и где смотреть
|
||||||
|
|
||||||
|
| Что | URL / SQL |
|
||||||
|
|---|---|
|
||||||
|
| Каталог | `/tests` |
|
||||||
|
| Редактор | `/tests/<id>/edit` |
|
||||||
|
| Версии теста | `GET /api/tests/<id>/versions` |
|
||||||
|
| Активность LLM | `/settings` + `POST /api/llm/ping` |
|
||||||
|
| `test_versions` | `SELECT id, version, is_active, parent_id, created_at FROM test_versions WHERE test_id = '<id>' ORDER BY version;` |
|
||||||
|
| Попытки | `SELECT id, test_version_id, status, created_at FROM test_attempts WHERE test_version_id IN (SELECT id FROM test_versions WHERE test_id='<id>');` |
|
||||||
|
|
||||||
|
При баге прикладывайте:
|
||||||
|
- скрин редактора / модалки;
|
||||||
|
- ответ соответствующего эндпоинта (DevTools → Network → JSON);
|
||||||
|
- результат SQL из таблицы выше;
|
||||||
|
- `docker compose -f docker-compose.dev.yml logs --tail=200 testing-flask`.
|
||||||
Reference in New Issue
Block a user