unica-testing · git:20261106.ef5aba3 · 2026-11-06 · sha256 9cdc423420129d09

unica-testing git:20261106.ef5aba3A

Immutable. This exact content never changes and is served at /api/v1/blob/9cdc423420129d09.

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

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

Сначала установи, какое нарушение должен обнаружить тест. Найди ближайшие
проверки и применимые правила в `arch/rules/` по предмету и исходнику, а тесты
правила — командой `python scripts/dev/rule-marks.py tests <ID>`;
прочитай их до выбора решения. Отсутствие записи не отменяет
гарантии продукта. Все команды ниже выполняются от корня репозитория;
`python3.12` и кавычки на Windows — по
[командам на разных ОС](../unica-development/SKILL.md#команды-на-разных-ос).

## Что дочитывать

Открывай справку, когда работа касается её предмета:

| Работа | Что открыть |
| --- | --- |
| `.github/workflows/`, отбор, итоговый шлюз CI, отчёты Allure, сайт и ночной ярус в `scripts/ci/`, сборка, передача и публикация артефактов | [Конвейер и отчёты](references/ci-pipeline.md) |
| Выпускаемый XML 1С, фикстуры БСП schema-v2, корпус и верификаторы 8.3.27 | [Проверка XML платформой](references/platform-xml.md) |

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

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

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

Не создавай тест, который только повторно вызывает уже зарегистрированные
проверки ради одной ссылки. Общие вспомогательные функции и самостоятельные
параметризованные проверки допустимы. Новый тест, подтверждающий правило,
несёт его пометку `rule: <ID>`; формат пометки и привязку задаёт
[unica-rules](../unica-rules/SKILL.md#проверки-и-разрывы).

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

При изменении данных для другого инструмента проверь пример с новой
особенностью после чтения штатным потребителем или совместимым парсером.
Наличие нужных строк до разбора само по себе не доказывает сохранность
данных после чтения.

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

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

- Логику проверяй рядом с Rust-модулем, взаимодействие — через реальные
  интерфейсы компонентов. Для запуска продукта и протокола выбирай подходящую
  интеграционную цель в `crates/<crate>/tests/`.
- Python-проверки CI и упаковки находятся в `tests/ci/`, инструментов
  разработчика — в `tests/dev/`. Архитектурное назначение теста не требует
  отдельного каталога.
- Изолируй временные файлы, процессы и состояние; освобождай ресурсы и при
  падении. Для unit-тестов `unica-coder` проверь существующие помощники в
  `crates/unica-workspace/src/test_support/shared.rs`: снимок дерева, идентичность пути,
  восстановление текущего каталога. Управляй временем и случайностью там,
  где они влияют на результат. Повтор, произвольный sleep или `ignore`
  не устраняет причину нестабильности.
- Тест не выходит молча, если хосту не хватает возможности: так он
  записывается как PASS. В `tests/platform/` и фасадах `src/platform/`
  возможность самой ОС помечай
  `#[cfg_attr(not(<ОС>), ignore = "host capability <x>: <почему>")]`.
  В остальных случаях — возможность ОС вне этих путей или зависящая
  от тома и прав — вызови
  `unica_process::test_support::skip_without_host_capability`, затем выйди.
  Механизм и контуры описаны в
  [ci-pipeline.md](references/ci-pipeline.md#возможности-хоста).

Атрибуты `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`. Страж ограничивает доступ
к переходам; он не доказывает поведение сценария.

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

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

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

Rust-тест объявляет размер сам, без регистрации и генератора. Тест, который
запускает процесс или сокет либо идёт дольше 3 с, кладут в `tests/`
(интеграционная цель — всегда `medium`) или в модуль `medium_tests`;
отдельный такой тест среди малых называют `medium_<имя>`. Фильтр
`kind(test) | test(/(^|::)medium(_|::|$)/)` в `.config/nextest.toml`
постоянный и от PR к PR не меняется. Вызов через helper может скрыть
процесс от взгляда: размер решает автор по тому, что тест делает, и
вопрос шаблона PR. Не обходи отбор ради зелёного PR: малый по имени тест
с процессом в PR идёт в ворота `pr` без срока `medium`.

Переименование модуля или функции меняет имя теста: объяви старые имена
строкой `Removes-test: <binary-id>::<путь>::*` в описании PR. Фильтры
`large` задаются вручную: согласуй отбор `queue`, `main`, `large`
и его timeout в `.config/nextest.toml`. Этот ярус выбирает точные имена
тестов в `daemon_receipt_ledger` и `unica_coder`; страж сверяет их с
атрибутами тестов и модулями исходников. Расширение на другие цели требует
изменения стража и проверки фактического отбора nextest.

Временный каталог `nightly-only` в том же файле — не размер. Он называет
тесты `medium`, которые очередь и `main` пока исключают, а `nightly-full`
и релиз исполняют: так тест ждёт зелёного Windows в полном nightly, не
скрываясь за `#[ignore]`. Страж требует точные имена существующих тестов
без `ignore`. Когда nightly подтвердит тест на всех его ОС, убери тест
из каталога, и он вернётся в очередь. Цель без оставшихся тестов убери
из `NIGHTLY_ONLY_TARGETS` в `tests/ci/test_gate_profiles.py`. Пустой
каталог снимается целиком: профиль `nightly-only`, его исключение
в `queue` и `main` и проверки стража. Профиль `queue` исключает тесты
каталога и при точном `-E`, поэтому изменённый тест каталога перед push
запускай его профилем, например
`cargo nextest run -p unica-coder --test platform_project_health --profile nightly-only -E 'test(=project_health_full_portable_repository_is_ready)'`.

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

Отдельно проверь 32 одновременные аутентифицированные отмены: каждая
завершается не позднее 125 мс на Windows, macOS и Linux на воспроизводимом стенде.
Пакет отмен внутри актора не доказывает этот сетевой сценарий.

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

## Снимки схем мутирующих инструментов

Входная схема каждого мутирующего нативного инструмента хранится снимком
`tests/fixtures/mutation-schemas/<инструмент>.json`: операция и схема без
`description`, ключи отсортированы, одно значение на строку. Тест
`native_mutation_surface_has_exact_operations_and_schemas` сравнивает их
со схемой из кода и называет инструмент и первую отличающуюся строку.
Если изменение схемы задумано, обнови снимки командой
`UNICA_UPDATE_SCHEMA_SNAPSHOTS=1 cargo test -p unica-coder --lib native_mutation_surface_has_exact_operations_and_schemas`
и проверь diff снимков как часть контракта. Команда также удаляет снимок
исчезнувшего инструмента; в PowerShell переменную задают как
`$env:UNICA_UPDATE_SCHEMA_SNAPSHOTS='1'`. Два изменения схемы в разных
местах сливаются построчно. Конфликт в снимке разреши повторной генерацией
после слияния кода и перезапусти тест без переменной.

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

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

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

В CI `--phase build` создаёт архивы и полные планы всех полос раннера
(`--prepared` — каталог архивов джобы, `--results` — каталог отчёта);
`--phase run` исполняет их в той же джобе без Cargo-компиляции. Состав полос
по профилю и ОС задаёт `ci_rust_lanes.lanes`. `--phase verify` сверяет
объединение результатов с полным планом по ревизии, ОС, конфигурации
и попытке workflow. Для локальной точечной проверки сохраняй обычный
nextest-вызов. Исключение — тесты на подготовленный бинарь `unica`
из ordinary-архива (возможность `archive-fixture-binary`): вне
`run-tests.py --rust-lane ordinary` (фазы `run` и `medium` по архиву
из `build`) они пропускаются с этой причиной. Раскладку джоб, подготовленный ordinary binary для Python
и теневой selector описывает справка
в [конвейере](references/ci-pipeline.md).

При изменении навыков или поверхности, охваченных `tests/agent_evaluation`,
запускай [приёмку агентом Codex](../../../CONTRIBUTING.md#приёмка-агентом-codex).
Набор выполняй только локально. Отсутствие CLI или авторизации не заменяется skip. Протоколы
и привязанный к их hash вердикт сохраняй в `.session-temp/acceptance-agent/`.
Успешный source-корпус не заменяет этот прогон.

CI выбирает профиль по событию: pull request исполняет `pr`, очередь слияния
в `main` — `queue`, push в релизную ветку — `main`; соответствие задаёт
`GATE_PROFILE` в `.github/workflows/unica-plugin-release.yml`. Поэтому
`medium` впервые исполняется в очереди, а у релизной линии без очереди —
после push в неё. Зелёный PR не доказывает зелёную очередь. Rust-тесты
на Windows идут только в ярусе `large`; Clippy под Windows исполняет очередь
и кнопка `main` отдельной джобой. Для разрешённой прозы PR и очередь
могут пропустить Rust-тесты и Clippy; смешанная или неизвестная группа очереди
сохраняет полный контур. Push в `main` повторно их не исполняет, а при изменении
CI/toolchain или неизвестном diff только прогревает зависимости. Границы отбора
и независимость линта от тестов описаны в [конвейере](references/ci-pipeline.md).
Для применения метки `ci:full` следуй
[порядку ворот pull request](../../../CONTRIBUTING.md#ворота-pull-request).

На PR тестов Rust нет, поэтому перед снятием черновика обязателен локальный прогон
затронутых крейтов:
`python scripts/ci/run-static-tests.py` и
`cargo nextest run --profile queue -p <крейт>`.

Перед push запусти `medium`-проверки, которые исполняют изменённое поведение.
В Rust это тесты затронутого модуля и цели `crates/<crate>/tests/`; запускай
их с профилем `queue`, он повторяет отбор очереди. Например, правку демона,
каталога инструментов или словаря `run` проверяет фильтр
`-E 'test(/^infrastructure::daemon::server::/)'`: профиль `pr` исключает весь
этот модуль. Правку словаря `can` проверяет
`-E 'test(/^infrastructure::daemon::v13_can_honesty_tests::/)'`: его
длительные проверки идут только с очереди. В Python прочитай короткий список `[medium]` в
`.config/python-sizes.toml` и выбери записи по предмету правки: например,
изменённое описание в `tool_catalog.rs` проверяет
`tests.ci.test_tool_surface_ledger`.

Правку Rust может остановить Python-страж: например, храповик отказов считает
места по всему дереву, и поиск по имени изменённого файла его не находит.
Перед push запусти малую полосу `tests/ci`, которую исполняют ворота PR:
`python3.12 scripts/ci/run-tests.py --profile pr --ecosystem python --suite tests/ci`.

На Windows эта полоса локально не зелёная и на чистом `main`: ворота PR
исполняют её на Linux. На хосте Windows 11 без режима разработчика
5 ноября 2026 года падали 122–126 из 1419 проверок, и все они
падают на `main`. Причины —
возможности хоста: нет права создавать симлинки (`WinError 1314`), дочерний
вызов `python3` попадает в заглушку Microsoft Store (код 9009), Git Bash
переписывает аргументы вида `origin/main:<путь>`, тесты процессов
и публикации каналов. Поэтому на Windows до PR запускай модули `tests/ci`
и `tests/dev` по предмету правки и стражи, которые называет этот скилл,
а всю полосу оставь CI. Падение такого модуля сверь с чистым `main` на том
же хосте: совпадающее — свойство хоста, новое — дефект правки. Тесты под
хост не ослабляй и skip не добавляй; исправление переносимости оформляй
отдельной задачей.

Наборы Python устойчивы к нагрузке CPU и могут идти параллельно со сборкой,
clippy и тестами Rust. Тест, чей исход зависит от скорости машины, — дефект.
Готовность жди по событию, а не сном; бюджет старта процесса и наблюдения
отделяй от проверяемого срока; где тест завершается событием, срок только
предохраняет от зависания. Проверяемый срок сохраняй, а фейк в таком тесте
заведомо не отвечает. Границу длительности выражай через контракт, например
«меньше времени жизни держателя», а не через запас на скорость раннера.

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

```sh
cargo nextest run -p unica-coder --lib --profile default -E 'test(/^infrastructure::workspace::medium_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 настроен повтор: зелёный итог после падения
первой попытки не закрывает вопрос нестабильности.

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

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

Падение по сроку не списывай на нагрузку без свидетельства: проверь `uptime`
и свои фоновые процессы, затем чередуй прогоны изменённого дерева и базовой
ревизии.

Затем сверь связанное обязательство и разбери расхождение по
[unica-rules](../unica-rules/SKILL.md#расхождение-с-кодом-или-тестом).

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