# Тестирование мира в R2D

Цель: дать тестам выражать **ожидаемое игровое поведение** в понятиях мира
(`#door`, `.enemy`), а не во внутренних структурах C. Тест не должен знать
устройство движка, если он не проверяет именно это устройство.

Статус: описание существующей практики + требования к новым тестам. Связано с
[ROADMAP.md](ROADMAP) (фаза 6), [RECORD_REPLAY.md](RECORD_REPLAY),
[AGENT_IMPLEMENTATION_RULES.md](AGENT_IMPLEMENTATION_RULES) (правило 7).

---

## 1. Уровни тестов

| Уровень | Что проверяет | Где | Как запускать |
|---|---|---|---|
| Модульные без движка | чистая логика подсистем `$.…` под QuickJS | `tests/js/*_test.mjs` (91 набор, сверка 2026-10-08) | `build/_deps/quickjs-build/qjs tests/js/<имя>_test.mjs` |
| Интеграционные в движке | поведение игры и API через агентский протокол | `tests/agent/*_test.py` (106 наборов, сверка 2026-10-08) | `python3 tools/run_tests.py` |
| Стражи документации | «в доке написано, что чего-то нет, а в коде есть»; у каждого модуля есть страница и тест | `tests/doc_claims_test.py`, `tests/doc_coverage_test.py` | `python3 tests/doc_...py` |
| C | физика/BSP и прочие ядра | `tests/bsp`, цели CMake | `cmake --build build` |

Раннер агентских тестов различает три исхода: `ok` (код 0 **и** есть хотя бы
одна проверка), `fail`, `skip` (код 0, но проверок не было — нет ассета,
нет дисплея). **«Пропуск» — это не «зелено»** ([AGENT_API.md](AGENT_API) §4).
Логи прогона — `build/test_<имя>.log`.

---

## 2. Как выглядит тест сегодня

Игровая сторона использует `$.test` (проверки копятся, падение решает
программа-агент):

```js
$.test.check($('.enemy').length === 5, 'врагов пятеро');
$.test.equal($('#hero').hp(), 100, 'здоровье целое');
$.test.near($('#hero').pos().x, 100, 0.5, 'игрок у отметки');
$.test.truthy($('#door').attr('open'), 'дверь открыта');
$.test.reset();  $.test.results();  $.test.report();
```

Реализация — [agent.js:137-182](https://github.com/Nikide/russiano2d/blob/main/src/highlevel/agent.js),
справочник — [HIGH_LEVEL_API.md](HIGH_LEVEL_API) §25.

Сторона теста (Python) управляет движком через протокол:

```python
with Agent(game="tests/fixtures/door", seed=7) as a:
    a.step(30)
    assert a.eval("$('#door').attr('open')") is True
    a.cmd("key", key="E", action="tap")
    a.step(30)
    state_before, state_after = ..., ...
```

Клиент: [tools/agent_client.py](https://github.com/Nikide/russiano2d/blob/main/tools/agent_client.py). Он **всегда** запускает
движок как `--agent --headless --fixed-dt 1/60` и умеет `--seed`
([agent_client.py:222-240](https://github.com/Nikide/russiano2d/blob/main/tools/agent_client.py)).

---

## 3. Утверждения в понятиях мира (фаза 6 сделана)

Целевой вид проверок — селектор вместо ручных сравнений:

```js
$.test.reset();
$.expect('.enemy').count(5);
$.expect('#hero').positionNear(100, 300, 1);
$.expect('#door').state('closed');
// …команды протокола key/step…
$.expect('#door').state('open');
$.test.report();
```

Сопоставление с тем, что было:

| Пожелание | Сегодня |
|---|---|
| `t.expect(sel).count(n)` | **есть**: `$.expect(sel).count(n)` |
| `t.expect(sel).exists()` | **есть**: `$.expect(sel).exists()` / `.empty()` |
| `t.expect(sel).state('open')` | **есть**: `$.expect(sel).state('open')` — читает свободный атрибут `state`, который игра ставит сама |
| `t.expect(sel).positionNear(x, y, eps)` | **есть**: `$.expect(sel).positionNear(x, y, eps)`, допуск по умолчанию 0.5 px |
| `t.expect(sel).health(100)` | **есть**: `$.expect(sel).hp(100)` (или `.prop('hp', 100)`) |
| `t.expect(sel).prop(имя, значение)` | **есть**: свойство узла или свободный атрибут |
| `t.expect(sel).within(other, dist)` | `$('.enemy').within('#hero', 500)` — метод обёртки ([ROADMAP.md](ROADMAP) фаза 1); в утверждениях пока нет |
| `t.load('…bscene')` | загрузка сцены игрой (`$.scene.load`) или `--scene` |
| `t.keyDown/t.keyUp/t.step` | команды протокола `key`/`keys`/`step` |

`$.expect` и `$.test` пишут в один счётчик, поэтому итог (`results()`/`report()`)
и снимок агента (`state.tests`) видят и то, и другое.

---

## 4. Скриншоты и семантика

Скриншоты допустимы там, где важен визуальный результат, но для игрового
поведения **предпочтительны семантические проверки**:

* плохо: «на скриншоте дверь выглядит открытой»;
* хорошо: `$.test.truthy($('#door').attr('open'), 'дверь открыта')`.

Визуальная регрессия может дополнительно проверять рендер. Скриншот делается
командой `screenshot` ([AGENT_API.md](AGENT_API) §3.6) и попадает в
артефакты прогона.

---

## 5. Артефакты при падении

Провалившийся тест должен оставлять то, что можно сразу исследовать:

* лог прогона `build/test_<имя>.log`;
* скриншот (`screenshot`);
* семантический снимок мира (`state` / `$.agent.snapshot()`);
* при возможности — запись ввода ([RECORD_REPLAY.md](RECORD_REPLAY)).

---

## 6. Детерминизм тестов

Интеграционные тесты запускаются с:

* фиксированным шагом (`--fixed-dt`);
* известным зерном (`--seed`);
* управляемым вводом (виртуальный ввод агента, `step` вместо `sleep`).

Раннер требует именно `step`, а не ожидание по времени
(`tests/agent/README.md`). Кадры идут только по
команде, поэтому прогон воспроизводим до кадра.

---

## 7. Критерии приёмки

* CI/headless-окружение способно прогнать игровой тест **без видимого окна**
  (`--agent --headless`). GitHub Actions
  ([build.yml](https://github.com/Nikide/russiano2d/blob/main/.github/workflows/build.yml)) настроен на нативную сборку и
  C-тесты по новому тегу; запуск GPU/агентских тестов на раннере ещё не настроен.
* При падении тест-раннер отдаёт достаточно структурированного контекста,
  чтобы диагноз был возможен без перезапуска.

---

## 8. Документация и код

Три расхождения, найденных аудитом 2026-10-07, закрыты в самих документах:
`$.agent` больше не приписывает себе проверки (они у `$.test`), у
`$.debug.profile()` больше нет аргумента, а лимит 256 у `$.world.raycastAll`
больше не обещается (лимит — только у нативных запросов к физике).

Общее правило: **каждый закрытый пункт работы исчезает из документации в том
же изменении** ([TASKS.md](TASKS) §6). Страж `tests/doc_claims_test.py`
ловит только известный ему список утверждений «этого нет», поэтому
расхождения в сигнатурах он не видит — такие правки остаются на ревью.

## RE2D BSP World (новый специализированный путь)

`build/tests/r2d_re2d_world_test` проверяет одинаковые XY на трёх этажах,
headroom, ray/circle-height и приватную RGBA/depth-композицию (ASan/UBSan).
`python3 tools/run_tests.py highlevel_re2d_bsp_world_test re2d_bsp_combat_test`
проверяет compositor и игровое демо с АК. Это отдельный путь от legacy
`highlevel_re2d_world_test`. Подробности: [аудит](RE2D_WORLD_GUIDE).

## Контроль репозитория

`python3 tests/repository_hygiene_test.py` проверяет мусор в tracked файлах,
локальные ссылки Markdown, контрольные суммы пакетов и содержимое архивов.
`ctest` пока не регистрирует C-тесты: запускайте собранные test executables
напрямую. Успешный пустой `ctest` не является доказательством проверки.

## Периодический контроль производительности `$`

Отдельный инструмент — `python3 tools/bench_api.py`. Он не подключён к
`tools/run_tests.py`, обычному CI или release pipeline: запускать вручную время
от времени, после изменений горячих путей или при подозрении на деградацию.
Перед замером собрать актуальный engine. Для другой сборки указать
`--binary PATH --cache PATH/CMakeCache.txt`.

Сценарии: `tools/bench_api_cases.json`; реальные runtime fixtures в
`tests/fixtures/bench_api`, кадровые нагрузки используют существующий
`tools/bench_highlevel.py`. [API_PERFORMANCE.md](API_PERFORMANCE) — таблица,
методика, машина и границы охвата. Сырые данные сохраняются в JSON.

```bash
python3 tools/bench_api.py
python3 tools/bench_api.py --baseline docs/benchmarks/api-performance.json \
  --json build/bench_api_next.json --md docs/API_PERFORMANCE.md
```

Не сравнивать чужие машины/OS/build/backend или разные workloads. Exit 3
показывает превышение одновременно относительного и абсолютного порога; сначала
повторить замер без фоновой нагрузки. Это сигнал к расследованию, не автоматический
приговор. Быстрые unit checks runner без performance run:
`python3 tests/bench_api_runner_test.py`.
