Дорого стоят не тесты
YAxUnit ставится за несколько минут, синтаксис теста читается без документации, первый тест на общий модуль пишется за четверть часа. Проблема начинается не здесь.
Запускаем прогон в пакетном режиме:
1cv8.exe ENTERPRISE /F "путь_к_базе" /N admin /CRunUnitTests=run.json
Вместо прогона открывается обычное окно конфигурации. Ни ошибки, ни предупреждения. В логе платформы пусто. В журнале регистрации — ни одной записи уровня «Ошибка».
Проверяем конфигурационный файл — валиден. Проверяем расширение — платформа показывает Активно = Да. Проверяем права — полные. И всё равно ничего не происходит.
За одну сессию я поймал четыре независимые причины такого поведения. Каждая по отдельности полностью блокирует прогон, и ни одна не даёт диагностического сообщения. Пока не устранены все четыре, наблюдаемое поведение одинаково: приложение открывается, каталог отчёта пуст.
Это не рассказ о невезении, а замер стоимости входа. Несколько часов уходит не на работу, а на выяснение того, что ничего не сломано — окружение просто собрано не до конца. И самое неприятное свойство этой цены: её платит каждый новый человек на проекте, заново и в одиночку. Знание о семи шагах живёт в голове того, кто их уже прошёл, и не переносится ни коммитом, ни ревью.
Именно из этого наблюдения выросло решение, о котором пойдёт речь. Оно касается не тестирования, а того, как вообще хранить состояние рабочего окружения.
Диагноз: это класс проблем, а не набор случаев
Механизм у всех четырёх причин один. Разбор параметров запуска обёрнут в Попытка, и при любом сбое YAxUnit молча откатывается на настройки по умолчанию. В умолчаниях reportPath пустой — отчёт писать некуда. Исключение не выбрасывается, в журнал ничего не пишется.
Отсюда формулируется класс:
Любое окружение, где неверная конфигурация неотличима от верной по наблюдаемому поведению, порождает молчаливые отказы.
YAxUnit здесь — только повод. Тот же класс дают:
- BOM в JSON-конфиге. Файл открывается, глазами выглядит нормально,
json.loadпадает — и мост перестаёт видеть все базы разом, а не одну испорченную. - Отсутствие BOM в
.ps1. PowerShell 5.1 читает кириллицу как ANSI и сообщает «Unexpected token» на строке, которая выглядит безупречно. - Аргумент с пробелом, склеенный в один. Платформа сообщает «Отсутствует файл базы данных
'Z:\1Cv8.1CD'» — про базу, которую никто не просил открывать. - Загруженное, но не применённое расширение. Список расширений показывает
Активно = Да, код при этом не работает.
Есть и более коварный подвид: диагностика существует, но выводится только на экран. Расширение, подключённое в безопасном режиме, честно сообщает «Расширение подключено в безопасном режиме. Чтение конфигурационного файла недоступно» — но в пакетном режиме на экран никто не смотрит. Снаружи это неотличимо от зависшего прогона: процесс жив, файл базы пишется, отчёта нет.
По времени такое тоже не опознать. Первый запуск свежевосстановленной демо-базы сам по себе тянет штатное обновление ИБ около минуты — ровно похоже на ожидание у диалога. Различать приходится по косвенному признаку: при этой ошибке лог прогона не создаётся вовсе.
Общее у всех: симптом не указывает на причину, а иногда указывает на ложную. Диагностика превращается в перебор гипотез, и её длительность зависит от везения.
С таким классом бесполезно бороться внимательностью. Внимательность — это ресурс, который тратится каждый раз заново и заканчивается к вечеру пятницы.
Почему привычные средства не закрывают проблему
Прежде чем предлагать решение, честно разберу те, что напрашиваются первыми.
| Средство | Почему недостаточно |
|---|---|
| Чеклист в вики | Устаревает молча. Читается после ошибки, а не до. Никто не проверяет, что он ещё актуален |
| Скрипт развёртывания | Воспроизводит семь шагов каждый раз, ломается на любом из них, сам требует поддержки |
| «Спроси коллегу, он настраивал» | Не переживает отпуск, увольнение и смену проекта |
| Инструкция в README | То же, что чеклист, только с иллюзией версионирования |
Отдельно про скрипт — он выглядит самым разумным вариантом и потому заслуживает разбора.
Скрипт описывает путь. Каждый его запуск — это семь шансов сломаться: обновилась платформа, изменился формат конфига, диалог безопасности появился там, где его не было. Скрипт при этом сам становится объектом поддержки: его надо чинить, тестировать и версионировать. Мы заменили одну хрупкую последовательность на другую, чуть более автоматизированную.
Причём проверить скрипт по-настоящему можно только одним способом — выполнить его и посмотреть на результат. То есть всё равно проверяется точка прибытия, а не маршрут.
Отсюда решение: если проверяется прибытие, то и хранить надо прибытие.
Оговорюсь сразу — это верно там, где прибытие можно заморозить. Есть случаи, где нельзя, и тогда скрипт возвращается в игру, но уже с дополнительными требованиями к себе. Разберу их в разделе о границах применимости.
Решение: состояние как артефакт
Вот те семь шагов, которые нужно выполнить, чтобы прогон в принципе стартовал:
- Создать базу и загрузить конфигурацию.
- Создать роль с системными правами.
- Завести пользователя ИБ с этой ролью и снять защиту от опасных действий (способа два, см. ниже).
- Загрузить YAxUnit и применить его (это разные операции).
- Снять «Безопасный режим» у расширений.
- Убедиться, что в конфигурации есть
ПриНачалеРаботыСистемы()без параметров. - Проверить, что расширения реально подключены в сеансе.
Все семь одинаковы для любой задачи и не зависят от предметной области. Значит, они выполняются один раз, а результат сохраняется как .dt-слепок.
Дальше новая база создаётся одной командой и сразу готова к прогону тестов.
Что внутри эталона
Снятая защита от опасных действий. Иначе любая служебная обработка вешает модальный диалог, который в пакетном режиме подтвердить нельзя — процесс висит до таймаута. Способов два, и они относятся к разным уровням:
- флаг у пользователя ИБ — свойство конкретной базы, попадает в
.dtвместе с ней; - маска в
conf.cfgплатформы — свойство установки, действует на все базы, чья строка соединения подходит под регулярное выражение:
DisableUnsafeActionProtection=.*TestBases.*
Для одиночной базы проще первый способ. Для каталога с тестовыми базами — второй: он ловит все базы разом, переживает добавление новых и не требует заходить внутрь каждой. Проверено на демо-базе с пользователями, у которых флаг не снимался: три прогона внешней обработки подряд, ни одного диалога.
Это не отменяет отдельного шага для расширений — безопасный режим расширения живёт внутри базы и маской не снимается (подробнее ниже, в каталоге отказов).
Роль с системными правами и флагом «Устанавливать права для новых объектов». Второе важнее первого: без него роль приходится дописывать после каждого нового справочника. С ним объектные права не перечисляются вовсе — они выдаются автоматически.
Модуль управляемого приложения с пустой процедурой:
Процедура ПриНачалеРаботыСистемы()
// Пустая намеренно: это точка расширения, за которую цепляется YAxUnit
// через &После("ПриНачалеРаботыСистемы") в своём модуле приложения.
КонецПроцедуры
Каркасные конфигурации модуля приложения не имеют вовсе — а без него перехватчику не за что зацепиться.
Режим совместимости не ниже 8.3.27. На 8.3.17 механизм расширения модуля управляемого приложения не работает, и тесты не запускаются.
YAxUnit, загруженный и применённый, со снятым безопасным режимом.
Чем за это приходится платить
Решение не бесплатное, и делать вид, что это не так, — плохая услуга читателю.
.dtбинарный: диффа не видно, ревью содержимого невозможно, конфликты слияния не разрешаются.- Нужен Git LFS — репозиторий растёт, обычным
git cloneбез настроенного LFS слепок не получить. - Эталон привязан к версии платформы и требует пересборки при её смене.
- Состав эталона — отдельное решение, которое надо ревизовать: добавилось требование к окружению — эталон устарел.
Это принимается сознательно, потому что альтернатива дороже: семь ручных шагов на каждого нового человека, каждый из которых ломается молча.
Проверка, что эталон живой
.dt переносит всё перечисленное целиком, включая применённые расширения и настройки пользователя. Это не предположение: слепок развёрнут в чистую базу, прогон стартовал и записал отчёт с exitCode = 0.
О том, почему критерий приёмки сформулирован именно так, — отдельный раздел ниже.
Что получилось в итоге: линия
Не фреймворк и не продукт. Набор соглашений и артефактов, который превращает путь от постановки задачи до зелёных тестов в предсказуемую последовательность.
Вход: текст постановки задачи.
Выход: работающая конфигурация, junit.xml с exitCode = 0 и отчёт функциональных тестов.
| Стадия | Чем закрыта |
|---|---|
| Новая база «под ключ» | эталонный .dt — одна команда |
| Метаданные из описания | генераторы из JSON-описаний |
| Загрузка и проверка | пакетный режим конфигуратора |
| Модульные тесты | YAxUnit |
| Функциональные тесты | Vanessa Automation |
| Перенос и версионирование | Git + LFS |

Схема стадий линии: от постановки до зелёных тестов
Объекты создаются из JSON-описаний, а не руками в конфигураторе. Регистр накопления целиком:
{
"type": "AccumulationRegister",
"name": "ОстаткиНоменклатуры",
"registerType": "Balance",
"dimensions": [
"Подразделение: CatalogRef.Подразделения | master, mainFilter",
"Номенклатура: CatalogRef.Номенклатура | req",
"Характеристика: CatalogRef.ХарактеристикиНоменклатуры",
"Партия: DocumentRef.ПриходнаяНакладная + DocumentRef.Сборка"
],
"resources": ["Количество: Number(15,3)", "Сумма: Number(15,2)"]
}
Такое описание версионируется, ревьюится в merge request и читается за десять секунд — в отличие от XML-выгрузки того же объекта на несколько сотен строк.
Тесты — двух уровней. YAxUnit проверяет бизнес-логику напрямую, Vanessa Automation — работу через интерфейс. Оба нужны: юнит-тест не поймает сломанную форму, а сценарий через интерфейс слишком дорог для перебора вариантов расчёта.
Контракты вместо договорённостей
Это принципиальный момент, из которого следует всё остальное.
Между стадиями передаются файлы с однозначными контрактами:
постановка → JSON метаданных → XML исходников → BSL модулей
→ тестовый модуль → junit.xml → вердикт
Ни одна стадия не полагается на «мы же выше договорились». Каждая получает на вход артефакт, который можно прочитать, проверить и передать дальше.
Главное следствие: контракт переживает исполнителя. Стадию можно отдать другому разработчику, другой модели или в CI — формат входа и выхода не изменится. Договорённость, живущая в переписке, так не умеет.
Отсюда ещё два полезных свойства.
Ошибка локализуется. Если junit.xml не сформировался, проблема между «тестами» и «прогоном», а не «где-то в процессе».
Подход переносим. Конкретные генераторы у вас могут быть другими, но JSON-описание объекта, чеклист готовности базы и формат отчёта — универсальны.
Критерий приёмки: проверяемый или никакой
Правило, к которому я пришёл на этой работе:
Если критерий приёмки нельзя проверить программой, это не критерий, а надежда.
Разберу на нашем же случае.
«База открывается» — не критерий. База прекрасно открывается в состоянии, когда тесты не запускаются: расширение не применено, обработчика нет, режим совместимости старый. Именно это и вводило в заблуждение дольше всего.
«Тесты зелёные» — не критерий. Кто смотрел, когда, на какой версии конфигурации, все ли тесты были в наборе? Утверждение не проверяемо задним числом.
«Файл junit.xml существует, exitCode = 0, число тестов совпадает с ожидаемым» — критерий. Проверяется программой, воспроизводится кем угодно, годится в CI без изменений.
Разница между вторым и третьим — не педантизм. Ровно из-за неё возник самый коварный случай во всей истории: тесты выполнялись, но отчёта не было. Прямые слэши в путях — YAxUnit не считал такой путь абсолютным и не писал файлы. Наблюдаемая картина — пустой каталог, то есть «прогон не стартовал». Реальность — прогон отработал полностью, что видно по данным, которые тесты создали в журнале регистрации.
Формулировка «тесты зелёные» здесь не помогла бы вообще: смотреть было не на что. Формулировка через файл и код возврата сразу указывает, где искать — между выполнением и записью результата.
Поэтому приёмка эталонов делалась так: слепок разворачивается в чистую базу, запускается прогон, проверяется наличие отчёта и код возврата. Не «выглядит рабочим», а предъявлен файл.
Проверка на сквозной задаче
Подход, не проверенный на реальной работе, остаётся презентацией. Полигоном стала первая из четырёх задач экзамена «1С:Специалист по платформе» — она достаточно велика, чтобы пройти всю линию целиком.
Постановка (в сокращении):
Компания собирает и продаёт компьютеры. Закупка деталей — «Приходная накладная», сборка системных блоков — «Сборка», продажа — «Расходная накладная». Учёт ведётся в разрезе подразделений, в каждом своя учётная политика: ФИФО, ЛИФО или по средней. Детали учитываются в разрезе характеристик через план видов характеристик. Нужны отчёты по остаткам и по продажам с прибылью.
Два решения, определившие остальное
Как совместить три метода списания в одном регистре. Регистр остатков ведёт измерение Партия — ссылку на документ поступления, заполняемую только на приход. Алгоритм выбирается по учётной политике подразделения:
- по средней — берётся агрегированный остаток без разреза по партиям;
- ФИФО/ЛИФО — читаются остатки в разрезе партий, партии перебираются по дате в прямом или обратном порядке.
Единая точка расчёта себестоимости для «Сборки» и «Расходной накладной». Это решение важнее первого. Дублирование логики между документами — типовая причина снижения оценки на экзамене, и в промышленной разработке ничем не лучше: расхождение между двумя копиями появляется на первой же правке.
Функция СписатьСебестоимость(Подразделение, Номенклатура, Характеристика, Количество, Дата) Экспорт
// Измерение регистра типизировано ссылкой, поэтому Неопределено
// передавать нельзя — ни в блокировку, ни в параметры запроса.
Если Характеристика = Неопределено Тогда
Характеристика = Справочники.ХарактеристикиНоменклатуры.ПустаяСсылка();
КонецЕсли;
СпособОценки = СпособОценкиПодразделения(Подразделение, Дата);
// Управляемая блокировка на время чтения и использования остатка:
// без неё два параллельных проведения спишут одну партию дважды.
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиНоменклатуры");
ЭлементБлокировки.УстановитьЗначение("Подразделение", Подразделение);
ЭлементБлокировки.УстановитьЗначение("Номенклатура", Номенклатура);
ЭлементБлокировки.УстановитьЗначение("Характеристика", Характеристика);
Блокировка.Заблокировать();
// ... чтение остатков и перебор партий
КонецФункции
Тесты
Шесть тестов: четыре на расчёт себестоимости и два сквозных сценария.
| Тест | Данные | Ожидание |
|---|---|---|
| По средней | партии 10×1000 и 10×1200, списываем 15 | одна строка, сумма 1650 |
| ФИФО | те же партии | 10×100 + 5×120 = 1600 |
| ЛИФО | те же партии | 10×120 + 5×100 = 1700 |
| Граничный случай | запрос 15 при остатке 10 | списывается ровно 10 |
| Сборка | 5 корпусов и 5 плат, собираем 2 блока | детали 5→3, блоки по себестоимости 2000 |
| Продажа | 3 принтера по 10000, продажа за 42000 | остаток в ноль, прибыль 12000 |
Граничный случай проверяет требование, за которое на экзамене снимают баллы: остаток должен выводиться в ноль одновременно по всем ресурсам регистра, и «в долг» списывать нельзя.
Результат прогона:
exitCode: 0 Всего: 6 | Успешно: 6 | С ошибками: 0
Результат прогона YAxUnit: два набора, 6 тестов из 6 успешно
Функциональный сценарий Vanessa Automation — 16 шагов через реальный интерфейс: открытие списков, создание элементов справочников через формы, проверка записи. Все шаги зелёные.
Аргумент, который стоит держать наготове
В спорах о целесообразности автотестов рано или поздно звучит: «зачем, у нас тестировщик всё проверяет руками». Вот фактический ответ из этой работы.
Тесты нашли в моём коде две ошибки.
Мин(). Такой функции во встроенном языке 1С нет — она есть только в языке запросов. Синтаксическая проверка /CheckModules её пропускает, ошибка вылезает только в рантайме.
Неопределено в типизированном измерении. Попытка передать его в управляемую блокировку даёт «Неверный тип значения» уже на Блокировка.Заблокировать().

Тест поймал ошибку: «Неверный тип значения» на вызове Заблокировать()
Обратите внимание на трассировку справа: показан не только текст ошибки, но и вся цепочка вызовов до строки в модуле. Поиск причины по такому отчёту занимает минуты.
А теперь главное. Обе ошибки прошли бы ручную приёмку — при условии, что проверяющий вводит данные с заполненной характеристикой. А он её заполнит: она в форме, поле на виду, сценарий выглядит естественно.
Ручная приёмка проверяет сценарий, который проверяющий придумал. Автотест проверяет тот, о котором не подумал никто.
Каталог молчаливых отказов
Всё перечисленное ниже ломает работу без внятной диагностики: либо вообще без сообщения, либо с сообщением, которое до вас не доходит.
| Что вы видите | Что на самом деле |
|---|---|
| Открылось обычное окно вместо прогона тестов | В конфигурации нет ПриНачалеРаботыСистемы() — или у неё есть параметры |
| Тесты выполняются, но отчёта нет | Прямые слэши в путях конфигурации запуска |
| Расширение «Активно = Да», но код не работает | Расширение загружено, но не применено |
| Прогон висит, нет ни отчёта, ни лога | Расширение подключено в безопасном режиме — сообщение осталось на экране |
Маска в conf.cfg прописана, диалоги никуда не делись |
Значение — регулярное выражение, а не список: test1;test2;test3 не совпадёт ни с чем |
В логе /Out вместо ошибки п»їРЎРёРЅ… |
Лог бывает UTF-16, UTF-8 с BOM и ANSI — разбор угадал не ту кодировку |
| «Пользователь ИБ не идентифицирован» | В базе есть пользователи, значит /N обязателен на любой команде |
| «Ошибка исключительной блокировки» перед обновлением | Базу держит открытый клиент или фоновое задание |
| «Нет права на запуск с требуемым режимом окна» | Не хватает прав MainWindowMode* |
| «Запрещён запуск внешних обработок» | Нет InteractiveOpenExtDataProcessors |
| Процесс запущен, окна нет, всё висит | Запуск через cmd /c start без интерактивного рабочего стола |
| «Отсутствует файл базы данных 'Z:\1Cv8.1CD'» | Путь с пробелами склеен в один аргумент |
| Мост перестал видеть все базы разом | В JSON-конфиге появился BOM |
| Скрипт падает на «Unexpected token» в русских строках | В .ps1 наоборот — BOM отсутствует |
Каждая строка здесь — цена одного незафиксированного параметра окружения. Таблица нужна тем, кто ещё не зафиксировал состояние; после фиксации она перестаёт быть актуальной.
Разберу четыре самых коварных.
Сигнатура обработчика. ПриНачалеРаботыСистемы() — предопределённый обработчик события, и его сигнатура строго без параметров. Стоит написать ПриНачалеРаботыСистемы(Отказ, ПараметрыЗапуска) — и это уже обычная процедура: платформа её как обработчик не вызывает, механизм расширения не срабатывает, диагностики нет.
«Активно» ≠ «применено». /LoadCfg -Extension кладёт расширение в базу, но не подключает его. Нужен ещё /UpdateDBCfg -Extension. При этом список расширений показывает Активно = Да — и это вводит в заблуждение. Достоверная проверка — сравнить два источника:
РасширенияКонфигурации.Получить(Новый Структура, ИсточникРасширенийКонфигурации.БазаДанных) РасширенияКонфигурации.Получить(Новый Структура, ИсточникРасширенийКонфигурации.СеансАктивные)
Есть в первом и нет во втором — не применено.
Дефолтный фильтр. При откате на настройки по умолчанию фильтр становится ["tests"] и совпадает по подстроке с именами вроде myproject_tests. Тесты запускаются, отчёт не пишется. Отсюда правило: «файла нет» не равно «тесты не запускались» — сначала проверьте журнал регистрации на предмет данных, которые создают сами тесты.
Маска, которая ничего не маскирует. Параметр DisableUnsafeActionProtection — это регулярное выражение, сопоставляемое со строкой соединения, а не перечень баз. Написанное по аналогии с другими настройками значение test1;test2;test3 ищет в строке соединения буквальную подстроку test1;test2;test3 и не находит её никогда. Ошибки не будет: параметр валиден, синтаксис корректен, диалоги продолжают вылезать. Надёжнее задавать маску по расположению — тогда она попадает во все базы каталога сразу и переживает их переименование. И не стоит писать .* без уточнения: это снимет защиту у всех баз установки, включая рабочие.
Где этот подход не работает
Границы применимости важнее списка достоинств: решение, у которого их нет, обычно просто плохо изучено.
| Ситуация | Что ломается | Чем заменять |
|---|---|---|
| Типовая на десятки-сотни ГБ | .dt нереален по времени выгрузки и объёму |
Постоянные слоты, скрипт развёртывания с докатом и реестр состояния (разбор ниже) |
| Команда работает через хранилище конфигурации | Конфигурация в эталоне быстро расходится с хранилищем | Фиксировать только окружение, конфигурацию брать из хранилища |
| В базе персональные данные | Эталон с данными нельзя раздавать разработчикам | Эталон только с настройками, данные — генератором |
| Обновление платформы | Эталон привязан к версии, тихо устаревает | Регламент пересборки и приёмки после каждого обновления |
| Правка одного метода в готовой конфигурации | Вся линия — оверинжиниринг | Здравый смысл |
Что происходит на большой конфигурации
Первый случай самый частый, и его стоит разобрать на числах — тем более что я в него уже упёрся.
Для проверки совместимости расширения с разными типовыми и релизами понадобился пул из пяти постоянных слотов. Порядок величин там другой: одна демо-база — около 4,3 ГБ, её выгрузка в XML — около 8,5 ГБ и 42–49 минут. Причём выгрузка занимает почти всё время развёртывания слота: скачивание дистрибутива 3–10 минут, распаковка меньше семи, восстановление базы 2–4, установка расширения с тестами полторы минуты, прогон тестов полминуты — и сорок с лишним минут на выгрузку.
Замораживать такое слепком бессмысленно: .dt пришлось бы пересобирать на каждый релиз каждой конфигурации, а хранить — десятками гигабайт.
Поэтому носитель состояния меняется, а принцип — нет. Вместо одного слепка используются два артефакта в паре:
Скрипт развёртывания с докатом. Тот самый скрипт, который выше я называл недостаточным, — но с двумя обязательными свойствами. Он проходит все шаги за один вызов и умеет продолжаться с прерванного места, не повторяя скачивание и распаковку. Без доката сорокаминутная выгрузка превращает любой сбой на последнем шаге в потерю часа.
Реестр состояния. Обычный файл, где записано, что именно сейчас развёрнуто в каждом слоте, какого релиза, когда собрано и в каком состоянии проверки. Он объявлен единственным источником правды и обновляется при каждой пересборке.
Разница с чеклистом принципиальная: чеклист описывает, что нужно сделать, реестр — что фактически сделано. Первое устаревает молча, второе расходится с реальностью заметно, потому что рядом лежат результаты прогонов.
Принцип сохраняется: состояние фиксируется в артефакте, приёмка идёт по машинно-проверяемому критерию. Меняется носитель, не идея. Для одиночной базы это слепок, для пула больших конфигураций — скрипт с докатом плюс реестр.
Стоимость владения
Что придётся платить постоянно, а не однократно.
Пересборка эталонов при смене платформы. Не автоматическая: собрать, развернуть в чистую базу, прогнать тесты, убедиться в exitCode = 0. Полчаса-час на каждый эталон.
Рост репозитория. Слепки хранятся в LFS, каждая версия — это мегабайты, которые не сжимаются и не диффятся. Историю приходится чистить осознанно.
Ревизия состава. Появилось новое требование к окружению — эталон устарел, и узнаете вы об этом по упавшему прогону, а не по предупреждению. Состав эталона — такой же объект поддержки, как код.
Актуальность пула. Там, где вместо слепков живут постоянные слоты, платить приходится и местом, и временем: каждый новый релиз — это ещё несколько гигабайт и около часа развёртывания, из которых сорок с лишним минут занимает одна только выгрузка. Плюс дисциплина обновления реестра: слот, пересобранный без записи в реестр, хуже отсутствующего — на него будут ссылаться как на проверенный.
Дисциплина приёмки. Соблазн сказать «база вроде открылась, наверное всё хорошо» никуда не девается. Критерий работает, только пока его действительно проверяют.
Итог честный: это дешевле, чем семь ручных шагов на каждого нового человека, но не бесплатно. Если в команде один разработчик и окружение не меняется годами — выигрыш сомнителен.
Следующий шаг: разделить линию между исполнителями
Разделение возможно потому что есть контракты. Без артефактов на границах любое разделение ролей превращается в переписку и согласования.
Постановка ↓ [архитектор] → JSON метаданных, архитектурные инварианты ↓ [исполнитель] → метаданные, модули, формы, отчёты ↓ [тестировщик] → тесты, прогон, junit.xml ↓ [ревьюер] → находки, severity, вердикт
Ревьюер обязательно на другой модели. Модель, написавшая код, плохо видит собственные ошибки — ей нужен независимый взгляд, а не самопроверка. Ровно та же логика, по которой автор не ревьюит свой merge request.
Тестировщик отдельно оправдан тем, что прогон — длинная операция, засоряющая контекст логами платформы, которые остальным стадиям не нужны.
Но есть и обратная сторона.
Дробить ради дробления вредно. Каждый агент стартует «холодным» и заново выводит контекст. Для правки одного метода цепочка из четырёх агентов — чистые потери.
Разделение ролей не решает главную проблему. Больше всего времени в этой истории ушло не на объём работы, а на диагностику молчаливых отказов. Агент упёрся бы в них ровно так же и потратил бы столько же. Лечит это зафиксированное состояние, а не организационная схема.
Начинать стоит с двух ролей — исполнитель и ревьюер. Это даёт основную пользу при минимальной сложности.
Что дальше
CI/CD. Прогон тестов на каждый push. Всё для этого готово: junit.xml — стандартный формат, exitCode — машиночитаемый результат. Критерий приёмки, сформулированный правильно с самого начала, переносится в пайплайн без переделки.
Остальные задачи экзамена. Бухгалтерский учёт, периодические расчёты, бизнес-процессы — линия та же, меняется только предметная область.
Перенос на рабочие конфигурации уже состоялся — учебная задача была полигоном, а проверка совместимости расширения потребовала пула из пяти постоянных слотов с типовыми и их разными релизами. Тесты там зелёные на каждом слоте, и линия отработала ровно то, ради чего строилась: на одном из релизов проверка показала несовместимость, которую иначе выяснил бы клиент.
Что изменилось при переносе — только носитель состояния и его цена (см. раздел о границах). Контракты, критерий приёмки и каталог отказов не изменились вовсе.
Решения, которые можно забрать
Даже если у вас другой инструментарий:
- Фиксируйте состояние окружения в артефакте, а не в документации. Документация не проверяется автоматически и устаревает молча. Носитель выбирается по масштабу: для одиночной базы — слепок, для пула больших конфигураций — скрипт с докатом и реестр того, что фактически развёрнуто.
- Формулируйте критерий приёмки так, чтобы его проверяла программа. «Открывается» и «выглядит рабочим» не годятся. Годится «файл существует, код возврата ноль».
- Считайте молчаливые отказы отдельным классом угроз и закрывайте их системно. Внимательность — расходуемый ресурс, а не механизм контроля.
- Передавайте между стадиями файлы с контрактами, а не договорённости. Тогда стадию можно отдать кому угодно, а ошибку — локализовать.
- Называйте границы применимости своих решений вслух. Решение без известных границ не изучено, а просто пока не сломалось.
Таблица «симптом → причина» выше — прикладной бонус: она экономит часы на диагностике, пока состояние окружения ещё не зафиксировано.
Инструментарий собран с использованием набора навыков для AI-агентов и MCP-серверов для 1С. В статье приведены собственные наработки: эталонные базы, чеклист готовности, диагностика молчаливых отказов и решение учебной задачи. Буду рад вопросам и замечаниям в комментариях — особенно от тех, кто ловил похожие отказы и знает строки для таблицы, которых у меня нет.
Вступайте в нашу телеграмм-группу Инфостарт