# Как устроена и расширяется система тестов

Раздел построен так, чтобы добавление контента (тестов, категорий, уровней, рангов, ачивок) не требовало правки движка.

## TL;DR

| Что делаешь | Минимальные шаги |
|---|---|
| Добавить **тест** (новый уровень в существующей категории) | 1) Создать `tests/<cat>/<level>.js` 2) Добавить `<script>` в три HTML |
| Добавить **категорию** | 1) Запись в `tests/_categories.js` 2) Папка `tests/<cat>/` |
| Добавить **уровень сложности** | 1) Запись в `tests/_levels.js` |
| Добавить **ранг** | 1) Запись в `tests/_ranks.js` |
| Добавить **достижение** | 1) Запись в `tests/_achievements.js` |

---

## Структура раздела

```
08-Tests/
├── index.html              # Главная: ранг + категории + ачивки
├── category.html           # Страница одной категории (?cat=linux)
├── test.html               # Раннер (?cat=linux&level=easy)
├── HOW_TO_ADD_TEST.md      # ← этот файл
└── tests/
    ├── _categories.js      # Реестр категорий (16 готовых)
    ├── _levels.js          # Реестр уровней (easy/medium/hard + задел)
    ├── _ranks.js           # Шкала рангов (8 шт.)
    ├── _achievements.js    # Достижения (12 шт.)
    └── <category>/
        ├── easy.js
        ├── medium.js
        └── hard.js

assets/
├── tests-engine.js         # Движок: рендеринг, XP, ранги, ачивки, runner
└── tests.css               # Стили
```

Идея: данные (категории, уровни, ранги, ачивки, тесты) — это **отдельные `.js` файлы** с глобальными переменными. Движок их находит и рисует. Никакого сборщика — открывается прямо из файловой системы.

---

## Хранение прогресса

В `localStorage` под ключом `devops-academy.tests-progress.v2`:

```js
{
  version: 2,
  tests: {
    "linux/easy": {
      bestPercent: 88,
      bestCorrect: 22,
      bestTotal: 25,
      attempts: 3,
      completed: true,    // ≥ passing хоть раз
      perfect: false,     // 100% хоть раз
      lastAttemptAt: "2026-05-16T..."
    },
    ...
  },
  achievements: ["first-blood", "century"],
  achievementsAt: { "first-blood": "2026-05-16T..." }
}
```

Сброс прогресса — кнопка в шапке каталога или вручную через DevTools.

---

## Как считается XP

Формула:

```
xp(test) = floor(level.baseXp × bestPercent / 100)
         + (perfect ? level.perfectBonus : 0)

totalXp  = Σ xp(test) по всем тестам
```

Дефолтные значения уровней (`tests/_levels.js`):

| Уровень | Вопросов | baseXp | perfectBonus | Max XP за тест |
|---|---|---|---|---|
| easy   | 25  | 25  | 10  | **35**  |
| medium | 50  | 100 | 30  | **130** |
| hard   | 100 | 400 | 100 | **500** |

При 16 категориях × (35+130+500) = **10 640 XP** на 100% всего. Шкала рангов подогнана: ранг **DevSecOps Master** требует 10 000 XP.

**XP начисляется только за лучший результат.** Повторное прохождение теста с худшим результатом — XP не даст. Лучший результат улучшил — получаешь только дельту.

---

## Шкала рангов

| Min XP | Звание | |
|---|---|---|
| 0 | 🌱 Стажёр | |
| 100 | 🧰 Падаван | |
| 350 | 🐧 Junior Engineer | |
| 900 | ⚙️ Middle Engineer | |
| 2000 | 🚀 Senior Engineer | (+ ачивка «Восходящая звезда») |
| 4000 | 🏗️ Tech Lead | |
| 7000 | 🛡️ Principal | |
| 10000 | 👑 DevSecOps Master | (+ ачивка «DevSecOps Master») |

---

## Добавить новый тест (новый уровень в категории)

Например, `tests/git/easy.js`.

### 1. Создай файл с вопросами

```js
window.TESTS = window.TESTS || {};
window.TESTS['git/easy'] = {
  category: 'git',
  level: 'easy',
  // passingScore: 70,  // по умолчанию берётся из _levels.js
  questions: [
    {
      q: 'Какая команда создаёт новый репозиторий?',
      options: ['git new', 'git init', 'git create', 'git start'],
      correct: 1,                  // индекс 0..3
      explain: 'git init инициализирует .git в текущей директории.',
    },
    // ... ровно 4 варианта на каждый вопрос
    // ... сколько вопросов укажешь — столько и будет (рекомендация: совпадает с _levels.js)
  ],
};
```

### 2. Подключи в трёх HTML

В `index.html`, `category.html`, `test.html` найди блок «ТЕЛА ТЕСТОВ» и добавь:

```html
<script src="tests/git/easy.js"></script>
```

Всё. Тест появится:
- на главной — в карточке категории Git
- на странице категории Git — как кликабельная карточка уровня
- через прямую ссылку `test.html?cat=git&level=easy`

---

## Добавить новую категорию

### 1. Запись в `tests/_categories.js`

```js
{
  id: 'rust-for-ops',
  group: 'Фундамент',         // существующая или новая группа
  order: 6,                   // порядок в группе
  title: 'Rust для инфры',
  description: 'Низкоуровневые тулы, eBPF, перформанс.',
  icon: '🦀',
  color: '#dea584',
},
```

Если ввёл новую `group` — добавь её в массив `window.TESTS_GROUPS_ORDER` в нужном порядке.

### 2. Создай папку `tests/rust-for-ops/`

Туда складывай файлы тестов. Категория без тестов будет отображаться с плашкой «тесты в разработке».

---

## Добавить новый уровень сложности

Например, **legendary** (200 вопросов).

### Запись в `tests/_levels.js`

```js
{
  id: 'legendary',
  title: 'Легендарный',
  short: 'legend',
  emoji: '🏆',
  color: '#7b42bc',
  questionCount: 200,
  baseXp: 1000,
  perfectBonus: 300,
  passingScore: 75,
  estimatedMinutes: 110,
  description: 'CKS‑class. Кто пройдёт — может смело идти на Principal.',
},
```

Уровень автоматически появится:
- как пилюля во всех карточках категорий на главной (серая, заблокированная)
- как карточка‑заглушка на странице категории
- активным станет, как только появится `tests/<cat>/legendary.js`

---

## Добавить новый ранг

В `tests/_ranks.js`:

```js
{
  id: 'guru',
  minXp: 15000,
  title: 'Кибер‑Гуру',
  icon: '🧙',
  color: '#5b3fa0',
  tagline: 'Открыл сам путь к Senior Vault Engineer.',
},
```

Движок сам отсортирует и подхватит. На странице результата теста, если пересечёт порог, появится плашка «Новый ранг!».

---

## Добавить достижение

В `tests/_achievements.js`:

```js
{
  id: 'k8s-warrior',
  title: 'K8s Warrior',
  description: 'Сдать все три уровня Kubernetes.',
  icon: '☸️',
  check: s => ['easy','medium','hard'].every(l => s.completedTests.includes('kubernetes/' + l)),
},
```

В `check(stats)` доступно:

| Поле | Что |
|---|---|
| `totalXp` | Суммарный XP |
| `attempts` | Сколько всего попыток сделано |
| `correctAnswers` | Суммарно правильных ответов (по лучшим попыткам) |
| `completedTests` | Массив id сданных (≥ passing) тестов: `['linux/easy', ...]` |
| `perfectTests` | Массив id, где было 100% |
| `categoriesTouched` | `Set` категорий, где есть хоть одна попытка |
| `categoriesCompleted` | `Set` категорий, где сданы все доступные уровни |
| `bestPercentByTest` | `{ 'linux/easy': 85, ... }` |
| `attemptsByTest` | `{ 'linux/easy': 3, ... }` |
| `allTestIds` | Массив всех зарегистрированных id тестов |
| `justFinishedTest` | id только что завершённого теста (null если проверка не из контекста финиша) |
| `justFinishedAttempts` | Попыток на этом тесте |
| `justFinishedPercent` | % последнего прохождения |
| `passingScoreOfLast` | Проходной балл последнего теста |

Достижение присваивается **один раз** при первом срабатывании.

---

## 🎯 Качество вопросов (обязательно к прочтению)

Цель — не «накидать вопросов», а сделать инструмент, который реально проверяет знания. Тест должно быть **сложно угадать** и **невозможно пройти без понимания темы**.

### 1. Откуда брать вопросы

- **Реальные собеседования.** Перед написанием — обязательный поиск: «<тема> interview questions DevOps/SRE», «топ вопросов на собесе по <теме>», англоязычные подборки (Glassdoor, LeetCode discuss, GitHub `awesome-*-interview-questions`, blog.interviewpal, interviewbit), русскоязычные (Habr, uproger, itvdn, Хабр Q&A).
- **Production‑инциденты.** Постмортемы (Cloudflare, GitHub, AWS), CVE-описания, описания багов в issue‑трекерах open‑source проектов.
- **Официальная документация.** RFC, документация ядра, man‑страницы — для точности формулировок.
- **Не выдумывать «из головы».** Если вопрос звучит надуманно — его не задают на собесе.
- **Англоязычные источники приоритетнее.** Большинство собеседований в индустрии — в английских терминах; перевод оставлять близким к оригиналу.

### 2. Структура варианта ответа

**ГЛАВНОЕ ПРАВИЛО:** все 4 варианта — примерно одной длины и одного стиля. Если правильный ответ — две строки с подробностями, то и три неправильных тоже должны быть на две строки. Никаких «огрызков» в 3 слова рядом с детализированным правильным.

❌ **Плохо** (глаз цепляется за длинный):
```
options: [
  'Алгоритм хеширования',
  'Заголовок Strict-Transport-Security, который принудительно переключает браузер на HTTPS для домена; защищает от SSL stripping',
  'Тип сертификата',
  'DNS-расширение',
],
```

✅ **Хорошо** (все на равных):
```
options: [
  'Алгоритм хеширования паролей на основе SHA‑2',
  'Заголовок ответа, принудительно переключающий браузер на HTTPS',
  'Расширение TLS, шифрующее SNI на стороне клиента',
  'Тип DNS‑записи, указывающий на политику HTTPS у домена',
],
```

Все подробности и нюансы — в поле `explain`. Сам вариант — компактная, лаконичная формулировка.

### 3. «Коварные» дистракторы (plausible misconceptions)

Неправильные варианты должны быть **частыми заблуждениями**, а не глупостями. Иначе кандидат с минимальной логикой отсечёт мусор и угадает.

❌ **Плохо** (откровенно бредовое):
- «Это шифрование» / «Замена DNS» / «Метод сжатия»
- «Никогда не используется»
- «Это про что‑то совершенно другое»

✅ **Хорошо** (звучит правдоподобно, ловит на типичной ошибке):
- Устаревшая, но похожая на правду версия (например `tcp_tw_recycle` вместо `tcp_tw_reuse`)
- Перепутанные термины (DNAT vs SNAT, A vs CNAME, L4 vs L7)
- Похожие, но неверные численные значения (порт 433 вместо 443, MTU 1480 вместо 1500)
- Реальный механизм, но из другой области (применяет UDP idle timeout к TCP)
- Аргумент, который часто слышишь от джуна на собесе и который *почти* верный

### 4. Ситуативные вопросы (troubleshooting)

Минимум 30% вопросов в `medium` и 50% в `hard` должны быть **сценарными**, а не «что такое X?».

❌ **Слабо** (определение):
> Что такое PMTUD?

✅ **Сильно** (сценарий):
> Пользователи жалуются: `curl` маленьких файлов работает, скачивание больших зависает на 30+ секунд. TCP‑соединение в `ss` видно как ESTABLISHED. Что вероятнее всего сломано?

✅ **Сильно** (продакшен‑симптом):
> На граничном NAT в `dmesg` мелькает `nf_conntrack: table full, dropping packet`. Какое из действий нерационально?

### 5. Распределение `correct` по позициям

Финальное распределение правильных ответов между индексами 0–3 должно быть **примерно равномерным** (±2 от идеального N/4). Иначе тест проходится спамом одной кнопки.

Перед коммитом — запусти небольшую проверку:
```bash
node -e "
  global.window={};
  require('./tests/<cat>/<level>.js');
  const t = Object.values(window.TESTS).pop();
  const d=[0,0,0,0]; t.questions.forEach(q=>d[q.correct]++);
  console.log('Распределение:', d);
"
```

### 6. Разделение профилей в Hard

`hard` — про глубокое понимание. На таких уровнях разумно держать **в одной категории один профиль**. Не смешивать в одной куче, например:
- **Cloud / Enterprise:** Kubernetes networking, Service Mesh, mTLS, Envoy, eBPF, QUIC
- **Carrier / Telecom:** BGP edge, MPLS L2VPN, SRv6, route flap dampening, NSO

Если тема большая и сильно разная по профилям — рассмотри отдельные категории (`networking-cloud`, `networking-carrier`) либо чётко обозначь в описании уровня, на какой профиль он рассчитан.

### 7. Поле `explain` — обязательно

Это образовательный момент, а не «правильный ответ ещё раз». В `explain`:
- Почему правильный ответ верный (механика)
- Где это встречается в реальной работе
- Какая частая ошибка приводит к выбору неправильного варианта
- Конкретные команды/параметры/RFC, если применимо

---

## Чек‑лист для нового теста

**Структура:**
- [ ] Файл `tests/<cat>/<level>.js` создан
- [ ] `window.TESTS['<cat>/<level>']` объявлен
- [ ] Каждый вопрос — ровно 4 варианта
- [ ] У каждого вопроса корректный `correct` (0..3)
- [ ] (Рекомендация) Количество вопросов = `questionCount` из `_levels.js`

**Качество (см. секцию выше):**
- [ ] Вопросы взяты из реальных источников (собеседования, постмортемы, RFC) — не выдуманы
- [ ] Длина и стиль всех 4 вариантов в каждом вопросе сравнимы (±50%)
- [ ] Дистракторы — правдоподобные заблуждения, а не очевидный мусор
- [ ] Подробности в `explain`, варианты — лаконичны
- [ ] Минимум 30% (medium) / 50% (hard) вопросов — ситуативные/troubleshooting
- [ ] Распределение `correct` по индексам 0..3 — примерно равномерное
- [ ] Профиль уровня единый (cloud ИЛИ carrier для hard), без смешения

**Интеграция:**
- [ ] `<script>` подключён в `index.html`
- [ ] `<script>` подключён в `category.html`
- [ ] `<script>` подключён в `test.html`
- [ ] Открыл главную — категория показывает свежий уровень
- [ ] Открыл `test.html?cat=<cat>&level=<level>` — раннер заработал
- [ ] Прошёл тест — XP начислился, прогресс категории вырос
