{ "index": 103, "slug": "editorial-2025-02-field-ai-code-verification", "title": "Как проверить AI-предложение кода до merge", "excerpt": "Зелёный happy path не доказывает сохранность API и правил доступа. На примере трёх границ разбираем отрицательные тесты, команды проверки и критерий готовности изменения.", "contentHtml": "
AI-ассистент предложил короткое исправление: основной тест прошёл, diff выглядел аккуратно, а запрос без роли всё равно получил доступ к операции. Такой дефект легко пропустить, потому что проверка ответа и проверка права отвечают на разные вопросы. Цена ошибки — не только повторная отладка: при неверной авторизации пользователь может прочитать или изменить чужой ресурс.
\nНадёжный вопрос перед merge звучит так: какой контракт меняется, где находится граница доверия и каким наблюдением можно опровергнуть предложение? AI не является доказательством. Он ускоряет перебор вариантов, но каждое принятое утверждение нужно проверить в коде, тесте и контексте системы.
\nНачните с diff, а не с объяснения ассистента. Запишите вход, выход, потребителя и правило, которое нельзя нарушить. Для функции доступа это субъект, действие и ресурс; для mapper — форма результата; для функции preview — неизменность входного состояния. Одно предложение может затронуть сразу несколько границ, даже если изменена одна строка.
git diff --check\ngit diff --unified=80 -- src/auth/can-edit.ts\nrg -n 'canEdit|authorize|role|tenant|owner' src test\nПервые две команды показывают пробельные ошибки и полный локальный контекст. Третья помогает найти параллельные точки входа, но не доказывает, что найден полный граф вызовов. Имена каталогов здесь примерны: в конкретном репозитории подставьте фактические пути, а результат поиска сопоставьте с маршрутом запроса.
\nПредставим mapper, который получает subtotalCents и taxCents. AI заменил поле ответа на total. Арифметика осталась верной, поэтому тест, сравнивающий только число, зелёный. Потребитель читает amountCents и получает undefined. Это не «мелкое переименование»: форма ответа — часть контракта.
Проверяйте ключи и способ чтения результата тем же кодом, который использует приложение. Если изменение имени действительно нужно, сначала меняется контракт и все потребители, а затем тесты. Переписать assertion под новый ключ — не исправление, пока владелец API не принял несовместимое изменение.
\nfunction toAmount({ subtotalCents, taxCents }) {\n return { amountCents: subtotalCents + taxCents };\n}\n\nconst result = toAmount({ subtotalCents: 1000, taxCents: 180 });\nif (JSON.stringify(Object.keys(result)) !== JSON.stringify(['amountCents'])) {\n throw new Error('response shape changed');\n}\nif (result.amountCents !== 1180) {\n throw new Error('amount calculation changed');\n}\nЭтот фрагмент проверяет учебный объект и порядок ключей, заданный самим примером. В production контракт лучше закрепить схемой или типом и тестом реального потребителя; здесь намеренно показана минимальная проверка, которую можно запустить без фреймворка.
\nВторая ловушка — побочный эффект под безобидным именем. Функция preview должна построить представление, но предложенная реализация присваивает draft.status = 'normalized'. Caller передал собственный объект и после preview ожидает исходный статус. Проверка только строки ответа этого не увидит.
Снимите состояние до вызова и сравните его после. Для простого плоского объекта пример выглядит так:
\nfunction preview(draft) {\n return { ...draft, status: 'normalized' };\n}\n\nconst draft = { id: 'd-17', status: 'new' };\nconst before = JSON.stringify(draft);\nconst view = preview(draft);\n\nif (JSON.stringify(draft) !== before) {\n throw new Error('preview mutated input');\n}\nif (view.status !== 'normalized' || draft.status !== 'new') {\n throw new Error('preview contract failed');\n}\nПоверхностная копия не защищает вложенные массивы и объекты: если функция меняет draft.items[0], понадобится глубокая копия, иммутабельная структура или отдельный тест на каждый изменяемый уровень. Поэтому нельзя автоматически заменить любой Object.assign на знак качества. Сначала определите владение данными и разрешённые мутации.
Третий пример — проверка роли. Условие actorRole !== 'viewer' блокирует viewer, но пропускает undefined, опечатку и любую новую строку. Happy path для editor и даже отрицательный тест только для viewer не закрывают эту дыру.
Если в учебном контракте разрешена ровно одна роль, разрешённое множество должно быть явным, а неизвестное значение — отклоняться:
\nfunction canEdit(actorRole) {\n const allowedRoles = new Set(['editor']);\n return allowedRoles.has(actorRole);\n}\n\nfor (const [role, expected] of [\n ['editor', true],\n ['viewer', false],\n [undefined, false],\n ['edtiro', false],\n]) {\n if (canEdit(role) !== expected) {\n throw new Error('unexpected decision for ' + String(role));\n }\n}\nЭто тест чистой функции, а не всей авторизации. В реальном запросе нужно проверить, откуда взялась identity, кто выбирает tenant и ресурс, где сервер принимает решение и что происходит при отказе. Проверка роли в браузере может улучшить интерфейс, но не должна выдавать доступ: решающий контроль находится на сервере, шлюзе или серверной функции.
\n| Сигнал | Что он доказывает | Чего не доказывает | Следующее действие |
|---|---|---|---|
| Unit-тест happy path | Разрешённый вход даёт ожидаемый результат | Отказ, форма ответа, побочные эффекты и соседний ресурс | Добавить отрицательный и contract-тест |
| Линтер или SAST | Найден или не найден известный шаблон | Бизнес-правило и смысл конкретного ресурса | Разобрать finding вручную по data flow |
| AI code review | Ассистент сформулировал комментарии в scope review | Полноту находок и обязательное решение владельца | Проверить комментарии и повторить review после нового diff |
| Ручной сценарий | Наблюдаемое поведение на выбранном окружении | Другие входы, окружения и параллельные entry point | Закрепить сценарий автоматическим тестом |
| Approval | Уполномоченный принял решение по известному scope | Отсутствие неизвестных рисков | Записать остаточный риск и условия отката |
Разные статусы не складываются в магическое «всё безопасно». У каждого сигнала есть scope и слепая зона. Например, GitHub описывает Copilot code review как источник комментариев; по умолчанию это не approval, а автоматическая оценка готовности сама по себе не считается обязательным подтверждением. Это важное различие между подсказкой инструмента и полномочием на merge.
\ngit diff --check. Сохраните команды и версии инструментов.merge означает, что заявленные проверки пройдены; revise — известен следующий фикс; block — есть расхождение, которое должен снять владелец риска.Минимальный журнал проверки может быть обычным текстом в описании изменения:
\nscope: src/auth/can-edit.ts, test/auth/can-edit.test.ts\ncontract: only editor may edit a resource in the actor's tenant\npositive: editor + own tenant -> allow\nnegative: viewer, missing role, other tenant -> deny\nside_effects: request context unchanged\nchecks: npm test -- can-edit && git diff --check\nowner: team-auth\nverdict: revise\nСтрока verdict: revise здесь не формальность: если не проверен чужой tenant, честный результат — не merge. Журнал не заменяет тест и не делает проект безопасным, но оставляет проверяемую связь между риском, наблюдением и решением.
Если форма API не совпала с потребителем, остановите подготовку merge и найдите владельца контракта. Если preview мутирует данные, зафиксируйте вход до и после, затем выберите иммутабельный результат или явно названную мутацию. Если запрос без роли или с чужим tenant получает доступ, блокируйте изменение до серверной проверки и негативного теста.
\nStop, revert и rollback — разные действия. Stop не меняет историю и лишь запрещает продолжать merge. Revert создаёт новое изменение, отменяющее конкретный коммит. Rollback возвращает уже доставленную систему к прежнему состоянию и требует отдельного плана для данных, миграций, флагов и совместимости. Не называйте блокировку rollback: это создаёт ложное ощущение, что восстановление уже подготовлено.
\nПримеры выше — небольшие чистые функции, а не доказательство безопасности приложения. Они не проверяют токены, identity provider, срок сессии, CSRF, rate limit, кеши, гонки, multi-tenant запросы, базу данных, конфигурацию шлюза или эксплуатационные права. Даже полный набор unit-тестов не исключает дефект в маршруте, middleware или другом entry point.
\nAllow-list подходит для показанного правила с одной ролью, но не заменяет policy engine для сложных правил, где важны атрибуты ресурса, отношения владельца, время и состояние. Для таких систем тестируйте таблицу разрешений и отказов на уровне API и интеграции. Секреты, персональные данные и production-токены нельзя помещать в prompt или учебный репозиторий; используйте обезличенные фикстуры.
\nОфициальные рекомендации также не являются сертификатом. OWASP говорит, что ручной security-review дополняет автоматические инструменты и особенно нужен для бизнес-логики, потоков данных и контекстных уязвимостей. NIST SSDF задаёт общий набор практик безопасной разработки и общий словарь, но не выбирает контракт конкретного сервиса. Эти документы помогают построить процесс, а границы и остаточный риск всё равно определяет команда.
\nAI-предложение готово к merge, когда можно независимо ответить на четыре вопроса: какой контракт оно меняет; какой позитивный и какой отрицательный путь проверены; где выполняется окончательная проверка доступа; какой владелец принял остаточный риск. К ответам приложены текущий diff, воспроизводимые команды и результат тестов.
\nЕсли хотя бы один вопрос остаётся без наблюдаемого ответа, меняйте статус на revise или block. Такой вердикт не обвиняет инструмент и не требует отказаться от AI. Он просто оставляет merge только для изменения, чья граница, проверка и ответственность видны.