docs: статус проекта, инструкция dev, обновление всех .md

- PROJECT_STATUS: что сделано (черновики, версии, разбор, каталог) и планы
- DEV_CONTOUR_USER_GUIDE: сценарии для проверяющих на dev-стенде
- README, ТЗ, card1, журнал, бэклоги, шаги 01–11+README, спринты, TEST_TABLES: ссылки и примечания
- backend/PROGRESS: ссылка на PROJECT_STATUS

Made-with: Cursor
This commit is contained in:
Константин Лебединский
2026-04-24 22:12:06 +05:00
parent 4801ea9f19
commit a68331c86b
27 changed files with 842 additions and 12 deletions
+2 -1
View File
@@ -4,7 +4,8 @@
**Карта больших кусков работ:** [card1.md](card1.md) (версии **V**, документ **D**, авторизация **HR A**).
**Идеи и пожелания (простой язык):** [BACKLOG_IDEAS.md](BACKLOG_IDEAS.md)
**Журнал проверок по спринтам (авто + ручные шаги для заказчика):** [TESTING_JOURNAL.md](TESTING_JOURNAL.md)
**Журнал проверок по спринтам (авто + ручные шаги для заказчика):** [TESTING_JOURNAL.md](TESTING_JOURNAL.md)
**Сводка «что сделано / что дальше» (простым языком):** [../PROJECT_STATUS.md](../PROJECT_STATUS.md) · [инструкция для dev-стенда](../DEV_CONTOUR_USER_GUIDE.md)
**Этап 1 (ТЗ §4)** — пять фич: 4.1–4.5 (части можно параллелить).
**Этап 2 (ТЗ §5)** — дашборды.
+2 -1
View File
@@ -2,7 +2,8 @@
*Язык простой, без жаргона разработки. Сюда попадает всё, что всплыло в обсуждениях и ещё не вошло в жёсткое ТЗ.*
**Как пользоваться:** приоритеты и «да/нет» фиксируем отдельно; пункты **не** удаляем — переносим в раздел **Решено** с кратким итогом, если идея закрыта или отклонена.
**Как пользоваться:** приоритеты и «да/нет» фиксируем отдельно; пункты **не** удаляем — переносим в раздел **Решено** с кратким итогом, если идея закрыта или отклонена.
**Что уже в продукте (кратко):** [../PROJECT_STATUS.md](../PROJECT_STATUS.md).
---
+26 -7
View File
@@ -8,9 +8,13 @@
Ниже в разделе B таблица — **журнал уже прошедших** шагов. Новые шаги приходят **сначала в чат**, потом дублируются сюда.
**Ветка / коммит последней привязки:** `dev` (обновлять при релизе на проверку)
**Ветка / коммит последней привязки:** `dev` (обновлять при релизе на проверку; актуализация документации 2026-04-24 — [../PROJECT_STATUS.md](../PROJECT_STATUS.md))
**Адрес стенда (когда появится):** *(заполнить)*
**Адрес стенда:** `http://localhost:8080` (UI; при стеке `docker compose -f docker-compose.dev.yml up` — тот же origin для `/api/…`).
**Актуальный UI (после 2026-04-24):** старт прохождения — **не** с карточки теста, а со **списка «Тесты»**: в каждой строке **справа** кнопка **«Пройти»**; **слева** — ссылка на карточку. Под названием — **«Автор: Вы»** или **«Автор: Фамилия И. О.»**; в шапке — **Фамилия И. О.**, полное ФИО в подсказке. После **«Завершить тест»** — **разбор** по вопросам; у автора в карточке — **«Прогоны и разбор»** по завершённым попыткам. Шаги **S1-07** и **S1-13** в таблице ниже описывают **старый** вариант («Старт/Начать попытку» на карточке) — оставлены в журнале как история. Регресс по новому потоку — **S1-14** и далее.
**Текущий шаг для ручной проверки (код в чате = тот же номер):** **S1-14** — см. раздел B.
---
@@ -24,7 +28,7 @@
|---|----------------|--------|------|
| A1 | В проекте есть миграция базы: связь версий «родитель» (`parent_id`) и правило «только одна активная версия на тест» | [x] `002_…sql` | 2026-04-24 |
| A2 | Линтер (`npm run lint`): **0 errors**; остаются **warnings** `no-console` в существующих файлах | готово (errors) | 2026-04-24 |
| A3 | `npm test` в `backend/`: hasAny + проверка Werkzeug-совместимых хешей (`src/**/*.test.js`) | [x] готово | 2026-04-25 |
| A3 | `npm test` в `backend/`: hasAny, Werkzeug, V.9 smoke, **D.2** `documentExtract``src/**/*.test.js` (10+ тестов) | [x] готово | 2026-04-25 |
| A4 | Запрос «здоров ли сервер» по адресу `/api/health` при запущенном backend | [x] `{"status":"ok"}` | 2026-04-24 |
| A5 | Реализация card1: API тестов/версий, черновик, HR-login (опц.), D.1 upload, UI списка/версий/черновика (в `dev`) | [x] код | 2026-04-25 |
@@ -38,11 +42,26 @@
|-----|------------------------------|-----------|------|
| S1-00 | Открыть `TESTING_JOURNAL.md`, просмотреть верх и раздел B; в таблице — строка S1-00 «ожидает…» | ОК | 2026-04-23 |
| S1-01 | Открыть `card1.md`, убедиться, что есть блок про V.1 / V.2 / V.3 (сохранение / форк) | ОК | 2026-04-24 |
| | *(дальше — по мере выдачи шагов в переписке)* | | |
| S1-02 | Открыть в браузере `http://localhost:8080` — должна загрузиться **страница входа** (заголовок «Клинические тесты» / «Войдите в систему», поля логин и пароль, кнопка «Войти») | ОК | 2026-04-23 |
| S1-03 | В браузере открыть `http://localhost:8080/api/health` — в ответе виден JSON c полем `status` со значением `ok` (страница не «404» и не пустая ошибка) | ОК | 2026-04-23 |
| S1-04 | С экрана входа войти: учётка **вашей** среды (локальный `users` в `clinic_tests` **или** при `HR_AUTH=1` — логин HR). После **«Войти»** должен открыться экран **«Тесты»** с **шапкой** (слева бренд, справа **Фамилия И. О.**/роль и кнопка **«Выйти»**; не обязательно полное тройное ФИО в одну строку). Список тестов может быть пустым. | ОК | 2026-04-24 |
| S1-05 | На экране «Тесты» в поле **«Новый тест — название»** ввести любое имя, нажать **«Создать»**. Должен открыться экран карточки теста (ссылка «← к списку», блок **Версии**, черновик и т.д.). | ОК | 2026-04-24 |
| S1-06 | На карточке теста в блоке **«Черновик (V.3)»** (при необходимости изменить текст вопроса) нажать **«Сохранить черновик»**. Под кнопкой появляется пояснение (например, что черновик применён) или пусто без ошибки на красном. Раздел **Версии** остаётся / обновляется без сообщения «Доступ запрещён». | ОК | 2026-04-24 |
| S1-07 | В блоке **«Прохождение (V.4)»** нажать **«Старт попытки»**. Под кнопкой/рядом появляется сообщение, что **попытка стартовала** (с id или без), без «Доступ запрещён» / без красного текста с ошибкой API. | ОК | 2026-04-24 |
| S1-08 | Нажать **«← к списку»** и убедиться, что **ваш тест** отображается в списке (название, строка с v… и фрагментом id активной версии). | ОК | 2026-04-24 |
| S1-09 (опц.) | В шапке нажать **«Выйти»** — должен открыться экран входа. Снова **«Войти»** с теми же данными — снова экран **«Тесты»** (список на месте). | ОК | 2026-04-24 |
| S1-10 | **История версий** (card1 V.7): в карточке теста видны **заголовок**, таблица версий (версия, активна, дата). Если **≥2** версий — нажать **«сделать активной»** на неактивной, согласиться в confirm; в таблице **текущая** переносится; в списке **«Тесты»** в метке строки обновился **фрагмент id** активной версии. | ОК | 2026-04-25 |
| S1-11 | **Публикация / V.6**: **«Скрыть из списка»** — в верхнем списке теста нет; на странице «Тесты» внизу блок **«Скрытые вами из списка»** — открыть карточку — **«Снова показать в списке»** — тест снова в верхнем списке. | ОК | 2026-04-25 |
| S1-12 | В блоке **«Содержание: вопросы…»** задать вопрос(ы) и варианты, отметить верные, **«Сохранить черновик»** — без красной ошибки; **История версий** / заголовок обновляются при необходимости. | ОК | 2026-04-24 |
| S1-13 | **«Начать попытку»** — открывается экран с вопросами (радио/чекбоксы); **«Завершить тест»** — виден результат: правильно из N, %, сравнение с порогом, без 400 «нет вопросов» при сохранённых вопросах. | ОК | 2026-04-24 |
| S1-14 | Экран **«Тесты»** (`/tests`): у строки с тестом **справа** видна кнопка **«Пройти»**; **слева** клик по названию открывает **карточку** без автоматического старта попытки. | *ожидает* | — |
| S1-15 | Со **списка** нажать **«Пройти»** у теста с сохранёнными вопросами: открывается экран попытки; **«Завершить тест»** — результат (корректно из N, %, порог), без красной ошибки API. | *ожидает* | — |
| S1-16 | **Карточка в режиме «не автор»** (сотрудник / другой пользователь): **нет** кнопки «Начать попытку»; есть короткий текст, что пройти тест из **каталога** кнопкой «Пройти» справа. | *ожидает* | — |
| S1-17 (опц.) | **Автор** в карточке своего теста: раздела **«Прохождение»** с «Начать попытку» **нет**; после **«Сохранить черновик»** сообщение о статусе — **под** кнопками в блоке **«Содержание: название, порог, вопросы»**. | *ожидает* | — |
*Старые номера S1-01… сведём к той же таблице, когда появятся экраны; формулировки шагов вы получите **только** в чате, по одному.*
**Итог спринта 1:** дата __________ комментарий заказчика одной фразой: _________________________
**Итог спринта 1:** дата **2026-04-25** комментарий заказчика одной фразой: **смоук + V.6V.7 (S1-02…S1-11) и сценарий черновик→прохождение (S1-12, S1-13) пройдены; карточка card1 в объёме приёмки сценария закрыта, остаётся бэклог D.2+ / V.9 E2E**
---
@@ -66,11 +85,11 @@
| Спринт | Тема простыми словами | Раздел A | Раздел B |
|--------|------------------------|----------|----------|
| 1 | Версии, история прогонов | приём (код в dev) | 2 + очередь S1-02+ |
| 1 | Версии, история прогонов | приём (код в dev) | S1-02…S1-13 ОК; **регресс UI:** S1-14… *(в процессе)* |
| 2 | *(по мере появления)* | | |
---
*Связанные файлы: [sprint-01-testing.md](sprint-01-testing.md) (черновик чек-листа), [card1.md](card1.md) (задачи).*
**Очередь ручного приёма card1 (шаги по одному в чате, затем в таблицу B):** S1-02 — миграции и старт; S1-03 — сценарий тест (черновик / попытка / смена активной); S1-04 — (при `HR_AUTH=1`) вход HR. Первый шаг после внедрения: см. **одно** задание в чате от ассистента.
**Очередь (по запросу / спринт 2):** закрыть **S1-14****S1-17** (новый сценарий «Пройти»); затем — регресс после релизов; **D.2D.5**; **V.9** E2E; углубление V.8 (назначения / «мои тесты») по card1.
+22 -2
View File
@@ -36,8 +36,8 @@
| V.5 | API: `GET /tests` (роль) — только активные цепочки; `GET /tests/:id/versions`; `POST /tests/:id/versions/:vid/activate` | 403/404 по политике |
| V.6 | API: `PATCH /tests/:id` (деактивация цепочки `tests.is_active` или отдельное поле) | Список пустеет, данные на месте |
| V.7 | UI автора: номер/метка версии, предупреждение при «после первой попытки», экран **история версий**, кнопка **сменить активную** (с confirm) | Смоук `sprint-01-testing.md` |
| V.8 | UI списки сотрудника/автора: **один** ряд на цепочку, без дублей версий | |
| V.9 | Интеграционные тесты API + регресс «разбор старой попытки» по старым `question_id` | |
| V.8 | UI списки сотрудника/автора: **один** ряд на цепочку, без дублей версий | `GET /tests/:id/summary` + упрощённая карточка для не-автора; список `GET /tests` с JOIN на активную версию |
| V.9 | Интеграционные тесты API + регресс «разбор старой попытки» по старым `question_id` | `backend/src/integration/v9card1.test.js` при `CLINIC_TESTS_INTEGRATION=1` и миграциях; без БД — skip |
| V.10 | *Продукт:* при новой версии `test_assignments` **не** переносим на новый `test_version_id`; старт попытки — по **активной** версии (см. [task.md §2.6](task.md)) | Зафиксировано в ТЗ |
---
@@ -87,3 +87,23 @@
- Проверки и журнал: [TESTING_JOURNAL.md](TESTING_JOURNAL.md)
- Старый чек-лист: [sprint-01-testing.md](sprint-01-testing.md)
- Анализ таблиц (если ведёте): [TEST_TABLES_ANALYSIS.md](../TEST_TABLES_ANALYSIS.md)
---
## Статус реализации (сводно, 2026-04-25+)
| ID | Статус | Комментарий |
| --- | --- | --- |
| V.1V.6, V.7, V.10 | Код + API + UI приёмка | [TESTING_JOURNAL S1-10, S1-11](TESTING_JOURNAL.md); публикация, скрытые, история. |
| V.8 | Расширен MVP | `GET /api/tests`**только свои** (`created_by`) **и** тесты с **назначением** на `users.id` (`test_accessService.js`). Карточка/попытка/starter/chain — та же логика. Старт **«Пройти»** в списке. Назначение: `POST /api/tests/:id/assign` (только **автор**), при production — `CLINIC_ASSIGNMENT_ENABLED=1` (`featureFlags.js`); в `development` включено. UI: блок «Назначение сотрудникам» + `GET /api/auth/dev/assignment-directory` при `assignmentUi` из `/api/auth/me`. |
| V.9 | Частично | + `v9card1.test.js`: 0 попыток → in-place; после попытки → форк, старая попытка на старых `version_id` / `question_id`. Запуск: `CLINIC_TESTS_INTEGRATION=1` + `DATABASE_URL` (или DB_*), `npm run migrate`. |
| D.1 | Готово | `POST /tests/import/document`, 400/413, multipart. |
| D.2 | Готово | `documentExtractService.js`: PDF (`pdf-parse`), DOCX (`mammoth`), TXT/MD. |
| D.3 | MVP (импорт) | `documentGenService.js`: вызов Chat Completions (DeepSeek по умолчанию или OpenAI), JSON-черновик → `generation.draft` в `POST /import/document`; `LLM_NO_JSON=1` при несовместимости API. |
| D.4 | MVP | Карточка теста: выбор файла, превью, «Вставить в поле вопроса» → тот же черновик. |
| D.5 | Готово | Временный файл удаляется после чтения; Nginx `client_max_body_size 10m`. |
| A.1A.4 | Код + compose | `HR_AUTH` + `HR_DATABASE_URL` в `docker-compose.dev.yml`. |
| A.5 | MVP | `mapHrRoleToApp` без полного RBAC из HR-таблиц. |
| UI | 2026-04+ | Список тестов: подпись **автора** (Вы / Фамилия И. О.); шапка: **Фамилия И. О.** Разбор попыток: API + UI после `submit` и маршрут `/tests/:id/attempts/:aid/review` ([PROJECT_STATUS.md](../PROJECT_STATUS.md)). |
**Следующий шаг по card1:** **V.9** — довести supertest/HTTP-регресс при необходимости; **D.3+** — отдельные кнопки в редакторе (сгенерировать/проверить/улучшить), ключ в БД, `/settings` (см. [sprint-02](sprint-02.md)); **A.5**`staff_role_assignments` / HR API; по желанию назначения по **отделу**. Навигация по сценариям: [DEV_CONTOUR_USER_GUIDE.md](../DEV_CONTOUR_USER_GUIDE.md).
+2 -1
View File
@@ -2,6 +2,8 @@
**Актуальный ведённый журнал (авто + ручные шаги, ответы ОК/не ОК):** [TESTING_JOURNAL.md](TESTING_JOURNAL.md) — *этот файл остаётся как подробный черновик сценариев*.
**Сводка «что в коде»:** [../PROJECT_STATUS.md](../PROJECT_STATUS.md).
Ниже: **автоматизировано / проверено при разработке** и **ручная приёмка** — дублирует структуру; статусы переносите в `TESTING_JOURNAL.md`.
**Окружение:** зафиксировать ветку/коммит, URL стенда, тестовые учётки (роль автора, роль сотрудника).
@@ -64,4 +66,3 @@ _Сценарии для прогона в «боевом» темпе, без
---
**Итог приёмки спринта 1:** дата __________, подпись/комментарий _________________________
+2
View File
@@ -10,6 +10,8 @@
**Данные:** БД `clinic_tests` на общем кластере; сотрудник в сценариях — `staff_members.id`; `telegram_id` — только справка; RBAC — из HR. См. [card1 (вступление)](card1.md#хранение-связь-с-сотрудниками-rbac-зафиксировано).
**Текущая реализация (сводка):** [../PROJECT_STATUS.md](../PROJECT_STATUS.md).
---
## Цель спринта
+73
View File
@@ -0,0 +1,73 @@
# Спринт 2 — тестирование (AI-помощники)
**Состояние продукта:** [../PROJECT_STATUS.md](../PROJECT_STATUS.md) · чек-лист импорта и API — ниже.
**Предпосылка:** спринт 1 (версии) принят.
**Секреты:** для стенда допускается отдельный ключ DeepSeek; в логах/скриншотах **не** светить полный ключ.
**MVP 2026-04 (импорт + LLM, до полного спринта 2):** в `backend` задать `DEEPSEEK_API_KEY` *или* `OPENAI_API_KEY``POST /api/tests/import/document` при загрузке файла возвращает `generation.draft` → в карточке теста, блок «Импорт из файла», кнопка **«Применить сгенерированный черновик»** → затем **«Сохранить черновик»**. Юнит-тесты: `documentGenService.test.js`.
---
## 1. Автоматизировано и self-check (разработка)
_Отмечайте [ ] → [x] по мере выполнения._
### 1.1 Безопасность и API
- [ ] Эндпоинты настроек не возвращают сырой ключ на клиент
- [ ] Ошибка при отсутствии/невалидном ключе — структурированная, с кодом/текстом для UI
- [ ] «Проверить подключение» при валидном ключе — успех; при фейле — сообщение об ошибке сети/401 и т.д.
### 1.2 Моки / контракты (по возможности)
- [ ] Парсинг JSON-ответов LLM: при невалидном JSON — пользователю сообщение, не 500 без текста
- [ ] Unit-тесты маппинга «ответ LLM → черновик теста/вопроса» (на фикстурах)
### 1.3 Смоук перед передачей
- [ ] Сгенерировать тест: только с названием; превью; применить — вопросы в редакторе
- [ ] Проверить тест: модалка с текстом
- [ ] Улучшить вопрос: чекбоксы, применение частично
- [ ] Дистракторы: к существующим ответам добавилось 3 варианта
- [ ] После применения AI-изменений сохранение теста согласовано с правилами **версий** (если были попытки)
---
## 2. Ручная приёмка (я / заказчик)
### 2.1 Настройки
- [ ] Сохранить ключ, обновить страницу — приложение не показывает ключ, но AI-функции работают
- [ ] Очистить/испортить ключ — AI показывает ошибку и **есть** переход/ссылка на настройки
- [ ] «Проверить подключение» отражает реальное состояние (успех/ошибка)
### 2.2 Уровень теста
- [ ] **Сгенерировать тест** недоступен при пустом названии; при заполненном — выдаёт осмысленный черновик, применяется **целиком** по кнопке
- [ ] **Проверить тест** — рекомендации читаемы, модалка закрывается, данные теста не портит без явного применения
- [ ] **Предложить улучшение** — сравнение было/стало, выбор чекбоксами, применяется только отмеченное
### 2.3 Уровень вопроса
- [ ] **Улучшить вопрос** — нет молчаливой перезаписи; подтверждение через чекбоксы/применить
- [ ] **Дистракторы** — три новых **не** заменяют старые ответы
- [ ] **Сгенерировать подсказку** — текст появляется в поле, можно отредактировать и сохранить
### 2.4 Версионирование + AI (регресс)
- [ ] На тесте **без** попыток: массовое применение AI не создаёт лишних версий бессмысленно (ожидание как в спринте 1)
- [ ] На тесте **с** попытками: осмысленные сохранения ведут себя по правилам 4.1 (новая версия при изменении)
### 2.5 Дизайн
- [ ] Кнопки, модалки, превью, состояния загрузки/ошибок **визуально** в одном ряду с остальным модулем (как спринт 1)
### 2.6 Качество UX
- [ ] Долгий ответ LLM: индикатор ожидания, нельзя «задвоить» запросы без контроля
- [ ] Понятные сообщения при сбое сети или API DeepSeek
---
**Итог приёмки спринта 2:** дата __________, комментарий _________________________
+65
View File
@@ -0,0 +1,65 @@
# Спринт 2 — Редактор тестов: AI-помощники (desktop web)
**Формат:** отдельное веб desktop-приложение (этап 1, фича §4.2).
**Граница спринта:** начинается **после** приёмки спринта 1; заканчивается готовностью **всех** функций AI-помощника из ТЗ (настройки ключа, проверка подключения, сценарии уровня теста и уровня вопроса) на базе **DeepSeek** и сохранения **идентичности дизайна** с остальным приложением.
**Предпосылка:** версионирование (спринт 1) работает; сгенерированные/изменённые черновики сохраняются в модель **текущей редактируемой версии** согласно правилам 4.1.
**Стек (целевой в ТЗ):** Python, FastAPI — **в репозитории TestingWebApp фактически Node.js + Express**, LLM через HTTP (OpenAI-совместимый API, в т.ч. DeepSeek); ключ в окружении или (в целевом виде спринта) в БД, не на клиенте. Сводка MVP: [../PROJECT_STATUS.md](../PROJECT_STATUS.md).
---
## Цель спринта
Реализовать **§4.2 ТЗ**: интеграция DeepSeek, страница настроек, все табличные функции уровня теста и вопроса, обработка отсутствия ключа, UI с превью и подтверждением (без «тихой» перезаписи там, где ТЗ требует сравнения с чекбоксами).
---
## Функции (контрольный список из ТЗ)
### Интеграция и настройки
- Ключ DeepSeek на `/settings`, хранение в БД, не отдаётся на фронт
- «Проверить подключение» — тестовый запрос
- Все AI-действия при отсутствии ключа — понятная ошибка + ссылка на настройки
### Уровень теста
| Функция | Критерий |
| --- | --- |
| Сгенерировать тест | Только при заполненном названии; превью; применение целиком |
| Проверить тест | Модалка с рекомендациями |
| Предложить улучшение всего теста | Постатейно было → стало, чекбоксы, применение выбранного |
### Уровень вопроса
| Функция | Критерий |
| --- | --- |
| Улучшить вопрос | Модалка, было/стало по частям, чекбоксы, **без** прямой замены без подтверждения |
| Дистракторы | +3 неправдоподобных варианта **добавляются**, не заменяют |
| Сгенерировать подсказку | Текст в поле подсказки; автор правит/удаляет (связь с §4.4 в следующих спринтах) |
---
## Технические подзадачи
| # | Задача |
| --- | --- |
| 1 | Модель настроек (ключ), API save/test, маскирование в ответах |
| 2 | Промпты и контракты JSON для каждой функции; валидация ответа LLM |
| 3 | Эндпоинты/сервисы: 6 сценариев + единая обёртка ошибок/квот |
| 4 | UI: кнопки в редакторе, модалки, превью «сгенерировать тест», дифы с чекбоксами |
| 5 | Соблюдение правил 4.1 при сохранении применённых AI-изменений |
| 6 | Логи без утечки ключа; rate-limit/таймауты по best effort |
---
## Вне спринта 2
- Медиа (§4.3), полноценное поведение подсказок в прохождении (§4.4 + §4.5) — отдельные спринты, если не входят в минимум для кнопки «подсказка» в редакторе
- Дашборды (этап 2 ТЗ), HR-интеграция, MAX
---
## Документ тестирования
Чек-лист: `sprint-02-testing.md`.
+2
View File
@@ -10,6 +10,8 @@
| Адресат | Константин Л. (разработчик) |
| Базовый репозиторий | https://git.pirogov.ai/l_konstantin/TestingWebApp |
**Состояние репозитория (не ТЗ, а факт):** ветка `dev` — [PROJECT_STATUS.md](../PROJECT_STATUS.md); как пользоваться локальным стендом — [DEV_CONTOUR_USER_GUIDE.md](../DEV_CONTOUR_USER_GUIDE.md).
## 1. Контекст и зачем это делается
### Зачем клинике система тестирования