---
name: unica-testing
description: Выбор, написание, запуск и ревью тестов самого Unica — граница проверки, размеры small/medium/large, Rust nextest, Python unittest и разбор падений. Используй при изменении проверок или проверяемого поведения; тесты прикладного кода 1С относятся к unica:test-authoring.
---

# Тестирование Unica

Сначала установи, какое нарушение должен обнаружить тест. Найди ближайшие
проверки и применимые правила в `arch/rules/` по предмету, исходнику и `check`;
прочитай их до выбора решения. Отсутствие записи не отменяет
гарантии продукта. Все команды ниже выполняются от корня репозитория.

## Что доказывать

Хороший тест различает допустимое поведение и конкретное нарушение контракта.
Назови такой контрпример: неверный результат, чужие данные, потерянное изменение,
запрещённая зависимость. Если проверка остаётся зелёной при этом нарушении,
она не доказывает нужную гарантию.

Для дефекта сначала добавь или уточни воспроизведение на текущем коде, запусти
его и убедись, что падение вызвано дефектом; затем исправляй код. Для новой
возможности выведи ожидаемый результат из согласованного поведения. Существующий
тест часто достаточно расширить: ссылка из архитектурного правила не требует
его отдельной копии. Если автоматическое воспроизведение недоступно, объясни
ограничение и содержательную ручную проверку; текстовую ошибку не закрепляй
тестом на наличие исправленной фразы.

Если правило проверяют несколько тестов, перечисли их в `check`. Не создавай
отдельный тест, который только повторно вызывает уже зарегистрированные тесты
ради одной ссылки из правила. Общие вспомогательные функции и самостоятельные
параметризованные проверки допустимы.

Проверяй результат и существенные побочные эффекты. Выбирай случаи отказа,
отмены, восстановления и конкуренции по риску изменения. Не подменяй проверяемую
гарантию mock-ответом и не вычисляй ожидаемый результат копией реализации.
Проверка границы кода может читать AST, зависимости, схему или манифест:
нужны примеры разрешённой и запрещённой конструкции, включая нарушение
под другим именем, если имя не является частью контракта.

Не добавляй тесты на фразы, заголовки, длину AGENTS, скиллов или архитектурных
записей; на наличие отчёта о прошлой ручной проверке; на служебный реестр
ради самого реестра. XML-фикстура, ответ MCP и конфигурация, которую читает
программа, могут быть полноценными входами теста. Отличай их от прозы,
наличие которой не доказывает поведение.

## Граница и место

- Логику проверяй рядом с Rust-модулем, взаимодействие — через реальные
  интерфейсы компонентов. Для запуска продукта и протокола выбирай подходящую
  интеграционную цель в `crates/<crate>/tests/`.
- Python-проверки CI и упаковки находятся в `tests/ci/`, инструментов
  разработчика — в `tests/dev/`. Архитектурное назначение теста не требует
  отдельного каталога.
- Изолируй временные файлы, процессы и состояние; освобождай ресурсы и при
  падении. Для unit-тестов `unica-coder` проверь существующие помощники в
  `crates/unica-coder/src/test_support.rs`: снимок дерева, идентичность пути,
  восстановление текущего каталога. Управляй временем и случайностью там,
  где они влияют на результат. Повтор, произвольный sleep или `ignore`
  не устраняет причину нестабильности.

Атрибуты `cfg` и `cfg_attr` с `receipt-ledger-test-support` ставь на целые
Rust items: например, функцию, модуль или `impl`. Не ставь их на отдельные
выражения, операторы, поля или ветви `match`; `not` с этой feature запрещён
и на целых items. Проверяй границу командой
`python3.12 scripts/ci/check-receipt-ledger-test-support-boundary.py`.
Этот страж проверяет размещение атрибутов, а не полную эквивалентность
обычной и тестовой сборки.

В сценариях ReceiptLedger переходы проверяемой попытки выполняет продуктивный
код. Тест получает ответ по протоколу и наблюдает состояние. Подготовка
начального состояния, внесение отказа и действия второго владельца допустимы
как явно названные фикстуры; они не должны подменять работу проверяемой попытки.
При изменении этой обвязки запускай
`scripts/ci/check-receipt-harness-boundary.py`. Страж ограничивает доступ
к переходам; он не доказывает поведение сценария.

При изменении сравнений с донором или обновлении его снимка прочитай
[порядок сравнения и обновления](references/donor-comparison.md).

## Размер и разметка

Размер описывает стоимость и зависимости запуска. Модульный, интеграционный,
контрактный — назначение и граница теста; это другая классификация.

| Размер | Выбор | Разметка в Unica |
| --- | --- | --- |
| `small` | Локальная проверка с малыми затратами, без запуска продукта и сетевого взаимодействия | Умолчание после исключения старших размеров |
| `medium` | Процесс продукта, сокет или интеграционное окружение | Rust: все Cargo integration targets (`kind(test)`) и тесты файлов с признаками процесса/сокета. Python: `[medium].entries` в `.config/python-sizes.toml` |
| `large` | Нагрузка, ёмкость или длительный сценарий с существенными затратами | Явные Rust-фильтры в `.config/nextest.toml`; Python-переопределения пока не поддерживаются |

Отдельных size-аннотаций в тестах нет. Rust-классификатор
`scripts/ci/size-filters.py` смотрит на весь исходный файл: даже чистый тест
в файле с `std::process`, `Command::new`, сокетом или `CARGO_BIN_EXE_` может
стать `medium`. Это приближение; вызов через helper может скрыть зависимость.
Не назначай размер только по имени теста и не обходи отбор ради зелёного PR.

После изменения состава таких Rust-тестов обнови генерируемые блоки командой
`python3.12 scripts/ci/size-filters.py --write`. Она вызывает `cargo nextest list`
и может компилировать тесты; запуск без `--write` тоже обращается к Cargo.
Фильтры `large` генератор не меняет: согласуй отбор `queue`, `main`, `large`
и его timeout в `.config/nextest.toml`. Сейчас этот ярус ограничен целью
`daemon_receipt_ledger`; расширение требует изменения соответствующего стража.

В Python укажи модуль, класс или метод: `test_module.Class.test_case`.
Манифест использует имена discovery без префикса `tests.ci` или `tests.dev`.
Страж замечает прямой вызов Cargo или импорт socket, но не все косвенные
зависимости. Одной секции `[large]` в TOML недостаточно: её runner не читает.
Если нужен Python `large`, сначала реши поддержку отбора в runner.
После изменения разметки запусти применимые `tests/ci/test_size_guard.py`,
`tests/ci/test_python_sizes.py` и `tests/ci/test_gate_profiles.py`.

## Что запускать

Начни с точного теста, затем проверь затронутое поведение и необходимые ворота.
Расширяй или повторяй запуск после изменений, сбоя, новой неопределённости
или требования CI. Успешный достаточный прогон не требует повторного большого
запуска ради уверенности. Исследования платформы за feature `research`
и команды `scripts/research/` выбирай только для соответствующей задачи.

`scripts/ci/run-tests.py` выбирает наборы целиком. `--profile pr` допускает
`small`, `queue`/`main` — `small` и `medium`, `release`/`all` — все размеры.
`--ecosystem rust|python|all` сужает экосистему; `--suite tests/ci|tests/dev`
и `--only-size` относятся только к Python. `--dry-run` лишь печатает команды.
Профиль `large` на Linux/macOS выбирает тяжёлые Rust-тесты, а на Windows —
весь Rust-набор, кроме одного явно исключённого теста; сверяй текущий override
в `.config/nextest.toml`. Python-наборы сейчас в `large` не запускаются.

Для точечной проверки используй nextest или unittest напрямую. Например:

```sh
cargo nextest run -p unica-coder --lib --profile default -E 'test(/^infrastructure::workspace::tests::git_worktree_boundary_prevents_parent_workspace_discovery$/)'
python3.12 -m unittest tests.ci.test_rust_platform_boundary.RustPlatformBoundaryTests.test_repository_currently_complies_with_platform_boundary
```

Подставляй фактический crate, target и полное имя теста. `default` избегает
исключения нужного `medium`-теста фильтром `pr`. Python требует 3.12 и
зависимостей из `tests/ci/requirements.txt`. Для отчёта набора используй
`--results <каталог>`; подробности — в
[CONTRIBUTING.md](../../../CONTRIBUTING.md#отчёт-allure-локально).

`all` не означает все features и ignored-тесты. Для условной цели проверь
`required-features` в Cargo.toml. Штатный runner отдельно запускает
`daemon_receipt_ledger` с `receipt-ledger-test-support`, когда профиль допускает
`medium` или `large`. При точечном запуске этой цели нужны
`-p unica-coder --features receipt-ledger-test-support --test daemon_receipt_ledger`;
не включай эту feature всему workspace: она меняет путь fail-stop.
Ignored-тест запускай осознанно через `--run-ignored only` с точным фильтром
и выполненными предусловиями из причины пропуска.

Убедись по выводу, что нужный тест действительно исполнился. Ноль тестов,
skip, list и dry-run не доказывают поведение. При неясном отборе проверь
`cargo nextest list` с теми же target, features, профилем и фильтром; команда
может компилировать. У nextest настроен повтор: зелёный итог после падения
первой попытки не закрывает вопрос нестабильности.

## Изменение конвейера и отчётов

При правках CI сверяй отбор с событием и профилем запуска. Результат должен
сохранять коммит, линию, профиль, раннер и попытку прогона. Разные раннеры —
разные случаи теста; повторы одного случая остаются видимыми. Отключённый тест
показывается как `skipped` с причиной автора. Если раннер не дошёл до теста,
сохрани `skipped` с причиной и ссылкой на прогон; состав из прошлого прогона
явно обозначь как восстановленный, а не как точный план новой сборки.

При объединении профилей сохраняй поздний результат случая вместе со всеми
его попытками. Пересборка прежних результатов не добавляет новый прогон в
историю. Только 404 при чтении сохранённых данных означает их отсутствие;
сбой загрузки не должен стирать историю или результаты другого профиля.
Память проверенной ночной вершины обновляет только состоявшийся `large`:
тег или отменённый прогон его не заменяет. Для публикации проверяй источник
и репозиторий прогона, а не только имя ветки. Красный прогон тоже результат.

Проверки преобразования и сохранения результатов находятся в
`tests/ci/test_allure_results.py`, `test_collect_results.py`,
`test_build_site.py`, `test_site_fetch.py` и `test_nightly_lines.py`;
связь событий с джобами — в `test_unica_workflow.py` и `test_gate_profiles.py`.
Это проверки данных и исполняемой конфигурации. Не заменяй их проверкой
наличия прежнего плана или этих инструкций.

При изменении хранилища квитанций сохраняй нагрузочный критерий: за 60 секунд
не менее 1920 полных циклов резервирования, малого конечного ответа и ACK
(32 в секунду), p99 не более 250 мс, без ошибок вместимости и хранилища;
очередь записи освобождается за 2 секунды. Проверяй на поддерживаемых ОС
и описанном стенде. Пропуск неподдерживаемого стенда не является прохождением;
модель с искусственными часами не доказывает реальную пропускную способность.
Отдельно проверь 32 одновременные аутентифицированные отмены: каждая
завершается не позднее 125 мс на Windows, macOS и Linux на описанном стенде.
Пакет отмен внутри актора не доказывает этот сетевой сценарий.
Недостающая проверка Windows, сетевой отмены и воспроизводимость стенда: https://github.com/IngvarConsulting/unica/issues/975.

При изменении сборки сохраняй сборку ядра и загрузчика одним обязательным
`cargo build --locked` в отдельный каталог целевой платформы на каждом раннере. Кеш ускоряет сборку, но не заменяет её;
его ключ учитывает ОС, цель, тулчейн и Cargo.lock без префиксного восстановления.
Сохраняй в отчёте цель, попадание в кеш и длительность сборки.

Между джобами передавай нужные архивы и метаданные, а не каталог Cargo или
полный набор инструментов. Промежуточные поставочные артефакты хранятся сутки;
данные тестовых отчётов и тонкий пакет для продвижения имеют собственные сроки.
Раннер проверяет состав, хеши, режимы и нулевые отметки времени
своего архива, затем запускает MCP из извлечённых байтов с проверенными движками.
При выпуске повторяй проверку на скачанных опубликованных байтах.

Публикация релизных артефактов относится к push тега. PR и ручная проверка
не публикуют их; размещение и продвижение каталога остаются отдельными
действиями. Итог PR принимает стабильный агрегирующий CI gate по результатам
всех обязательных джоб, включая условно исполняемые. Проверки отбора и шлюза —
`tests/ci/test_unica_workflow.py` и `tests/ci/test_evaluate_ci_gate.py`.

## Проверка XML платформой

Фикстуры БСП schema-v2 — объявленная проекция XML 2.21 в профиль 2.20.
`harvestedSize` и `harvestedSha256` описывают исходный материал, `size` и
`sha256` — сохранённые байты фикстуры. Не называй их побайтной копией выгрузки
и не применяй преобразование фикстуры к исходникам пользователя.

При изменении выпускаемого XML сверяй корпус профиля и точный прогон
`scripts/dev/verify-8-3-27-platform.py`. Проверка XML по XSD не заменяет
импорт и повторную выгрузку закреплённой платформой: если она нормализует
или отвергает разрешённую схемой конструкцию, учитывай воспроизведённое
поведение платформы. Два изолированных цикла проверяют приёмку и семантическую
устойчивость выбранных случаев, а не все сочетания аргументов и не побайтовую
каноничность. Если нужная платформа недоступна, явно укажи непройденный прогон.

При обновлении корпуса сверяй нормализованный контракт двух независимых
порождений; сырые UUID могут различаться. Закреплённые хеши платформы,
установки и корпуса находятся в верификаторе и его JSON-профиле —
не восстанавливай их по исторической записке и не объявляй новый прогон
успешным по старому digest. Сохраняй проверку файлов и пустых каталогов.
`inconclusive` статического XSD не означает `pass`, а запуск с `--case`
не заменяет полный платформенный гейт. Отчёт различает исходный экспорт
и подготовку для повторного импорта: для XDTO верификатор может удалить
модуль, который отсутствовал в исходном корпусе, появился при экспорте
и побайтово совпал с `Package.bin`. Авторский модуль сохраняется
даже при таком совпадении. Эта компенсация верификатора не разрешает
автоматически очищать рабочие исходники. Такой сценарий не доказывает повторный импорт
дословного экспорта без этой компенсации.

Корпус создаёт ignored-тест `generate_platform_xml_corpus` цели
`format_8_3_27_xml_corpus`; выход задаёт `UNICA_XML_CORPUS_DIR`.
Статическую проверку выполняет `scripts/dev/verify-8-3-27-xml.py`,
платформенную — `scripts/dev/verify-8-3-27-platform.py`.
Каталоги корпуса и evidence должны быть отдельными и пустыми перед запуском;
справку аргументов бери у текущих скриптов.

Принятый платформой XML не доказывает передачу поля входного описания,
которое писатель вообще не выпустил: это проверяется отдельно по предметному
результату преобразования.

## После падения и при ревью

Различай нарушение assertion, сбой сборки/окружения и отсутствие исполнения.
Из свежего вывода или отчёта получи полное имя и traceback; target, профиль
и features сверь с фактической командой. Allure хранит Rust `fullName` как
`binary::module::test`, Python — как `module.Class.test`; features в нём
не записываются. По имени найди исходный файл и прочитай тело теста вместе
с вызываемым кодом.

Затем ищи в `arch/rules/` через `rg -F` адрес `путь::имя_теста` или
`путь::Класс.метод` из поля `check`, прочитай найденные обязательства.
`fullName` отчёта не равен адресу `check`. Ноль или несколько правил —
допустимый результат; не создавай запись ради заполнения связи. Runner
не подгружает архитектуру автоматически. После потери контекста восстанови
затронутые правила и тесты по файлам, не рассчитывай на память сессии.

При ревью проверь, что тест исполняет обещанный сценарий, обнаруживает его
нарушение и не расширяет доказанную гарантию. В результате работы назови
проверенный набор и существенные ограничения: пропуски, features, ОС,
неустранённые падения. Отдельный реестр проверок не нужен.
