Интересно, что принцип "короткой памяти" здесь фактически заменяет историю диалога текущим состоянием системы. История чата накапливает не только полезный контекст, но и шум. Код в этом смысле выступает как источник истины о том, что система представляет собой сейчас.
Но есть и обратная сторона. Код хорошо хранит состояние системы, но гораздо хуже хранит контекст принятия решений. По коду обычно видно, что сделано, но не всегда понятно, почему именно так.
Для автономных модулей такой подход выглядит очень практичным. Для долгоживущих систем это наверное открытый вопрос - где хранить не только текущее состояние, но и reasoning, который к нему привёл.
Самый интересный технический вывод здесь для меня это не метафоры, а промпты-абракадабра. Если эволюционно отобранная бессмыслица даёт сопоставимый прирост точности, то семантическое содержание системного промпта, по крайней мере в этих экспериментах, не выглядит необходимым условием. В статье предполагается влияние структуры и распределения токенов но это пока наблюдение по поведению black box, не объяснение механизма.
Ещё важное различие, которое в статье не очень акцентировано: абракадабра повышает точность, но для behavioural steering уже нужен связный нарратив. Похоже, два разных эффекта с разными механизмами.
С точки зрения безопасности, если управление поведением прячется в безобидной метафоре, традиционные лексические фильтры оказываются не слишком надёжным инструментом обнаружения.
Согласна. В статье я сфокусировалась на семантике soft delete и недостаточно раскрыла вопрос атомарности операции.
В реальной реализации такие изменения действительно должны выполняться в транзакции. Моя мысль была скорее о том, что даже при корректной технической реализации остаётся вопрос бизнес-семантики: что именно означает “удалить пользователя” для waitlist, appointment и других связанных компонентов.
Согласна, что показанный фикс решает только конкретный сценарий с waitlist и сам по себе не отвечает на вопрос, что делать с остальными связанными сущностями.
Наверное, моя формулировка про то, что “promoteFromWaitlist написан правильно” была слишком категоричной. Точнее было бы сказать, что функция была написана в соответствии со своими локальными правилами и не учитывала состояние deletedAt. Именно это расхождение между локальной логикой компонентов и привело к появлению ghost appointment.
Собственно, это и было основным наблюдением из инцидента: soft delete существовал на уровне users, но не был частью общего контракта системы.
Согласна, транзитивное покрытие действительно закрывает кейс со screenshot-тестами, пример был неудачным. Если изменение во View C, тест для View A через execution path подтянется.
Coverage и impact graph отвечают на разные вопросы.
Coverage-based selection отвечает: какие существующие тесты исполняли изменённый код. Impact analysis отвечает: какие компоненты зависят от изменения и есть ли для них тесты вообще.
Для запуска тестов coverage часто оказывается сильной эвристикой. Но граф ещё показывает пробелы: в статье отчёт явно говорит "ButtonBarBase без screenshot-теста" - если теста нет, coverage его не найдёт, нечего выбирать.
Скорее вижу эти подходы как дополняющие, а не конкурирующие.
Из опыта тестирования roguelike-движка: несоответствие механики и контекста часто не видно на этапе проектирования - оно проявляется только в прогонe игры и поведении системы. Механика случайного урона задумывалась как источник вариативности. Но анализ прогонов показал: в сценариях с одним врагом она почти не влияет на исход - RNG фактически "схлопывается".
Похожая история была с комбинацией bleed + defend - она не проектировалась как доминирующая стратегия, но в данных оказалась устойчивым паттерном, который усиливает сам себя. Поэтому "не конфликтуют" и "усиливают друг друга" - разные уровни проверки. Первое видно в дизайне, второе только в данных прогонов.
Подход с пересечением coverage и diff хорошо работает для тестов, где есть прямая связь "код → поведение" (unit / integration логика). Но у него появляется слепая зона, если перенести его на дизайн-систему и screenshot/visual regression тесты. В таких тестах покрытие строк кода вообще не коррелирует с тем, что мы проверяем. ButtonBase может покрывать десятки строк, но тест утверждает не выполнение кода, а визуальный контракт компонента.
Если ButtonCore меняет отступы, тени или layout-логику - coverage и diff покажут "затронуто", но не дадут ответа, какие визуальные контракты сломались. А именно это и есть цель теста.
Граф зависимостей из статьи решает другую задачу - не "какие тесты запускать по строкам", а "какие контракты системы потенциально затронуты изменением". В UI-слое coverage часто не является хорошей прокси для риска.
Хочу добавить практический кейс, который попадает в серую зону между stub и fake (и частично mock, если смотреть на поведение).
В моем проекте AI-эндпоинт POST /ai/recommend-doctor работает с флагом AI_MOCK_RESPONSE=true. В этом режиме внешний LLM (Claude) отключается, но retrieval layer и БД продолжают работать полностью в реальном режиме. Меняется только финальная генерация ответа. По сути это не чистый stub и не fake - это гибрид: часть системы настоящая, часть заменена, но поведение остаётся близким к production pipeline.
Интересный эффект проявился в CI. Тест ожидал Pediatrician, но получил General Practitioner. Claude в этом режиме не вызывался, значит проблема была не в LLM, а в retrieval layer.
В production этот баг был бы легко "замаскирован": LLM мог бы скорректировать неправильный top-1 и тест прошёл бы зелёным. Mock mode убрал этот компенсирующий слой и показал реальную проблему ранжирования.
Получается, что частично мокнутая система иногда даёт более честный сигнал, чем полностью интегрированная - потому что убирает "умный буфер", который скрывает ошибки upstream.
Самый полезный вопрос для ревью AI-сгенерированного теста у меня обычно не "какое покрытие он дал?", а "какой баг он должен поймать?".
Столкнулась с этим на сценарии для экрана AI-рекомендаций врачей. Генератор предлагал проверки в духе "результаты отображаются" и "навигация работает". Формально всё зелёное. Но реальное требование было другим: пользователь должен явно видеть способ обойти рекомендацию и перейти к полному списку врачей. Фича существовала технически, но была плохо обнаружима в контексте экрана.
Тест, который в итоге оказался полезным, проверял именно наличие видимой кнопки browse-all-doctors в результатах рекомендации. Большинство автоматически сгенерированных вариантов до такого требования не доходили, потому что воспроизводили текущую реализацию, а не бизнес-ожидание.
Публичных материалов практически нет, много концептуального про non-deterministic testing, но очень мало конкретных практик с кодом.
Из своего: разделила AI-тесты на два условных слоя: contractLayer (domain invariants + hallucination detection, работает с моком без реального Claude) и qualityLayer (metamorphic consistency + statistical assertions, только с реальной моделью). Технически 2 describe с разными условиями запуска. Это для одного AI-эндпоинта. Для полной оркестратор-воркер цепочки coverage будет другим. Складывалось по частям, не как система сразу. Этот gap между “концепция” и “вот рабочий код” - то, что хочу со временем закрыть в своём контенте.
Интересный разрез про latency и chunking. В RAG есть более базовый слой - это наблюдаемость retrieval. На практике ключевая проблема не в том, "что вернул поиск", а в том, какой именно контекст оказался в prompt на конкретном шаге. При эвристическом ранжировании это часто неочевидно и плохо воспроизводимо.
В одном RAG-пайплайне тест падал с 422 и первопричина оказалась в retrieval-слое: релевантный контекст не попадал в LLM из-за логики матчинг/скоринга. На уровне симптомов это выглядело как ошибка модели, но LLM просто работал с некорректным входом. После изоляции retrieval через mock-режим стало видно, что проблема находится до генерации.
В этом смысле top-k, chunk size и retrieval strategy - это не только параметры качества и latency, а границы наблюдаемости: они определяют, можно ли вообще восстановить и зафиксировать вход системы в момент ответа.
Самое ценное в такой архитектуре с точки зрения тестируемости это связка causation_id + immutable log.
У меня в RAG-пайплайне был кейс, где тест падал с 422 (неправильная специализация). Проблема была не в модели, а в том что retrieval layer формировал некорректный контекст.
Ключевой вопрос в отладке был не "ошибся ли LLM", а "какие именно данные попали в контекст на этом шаге". Без иммутабельного лога это требовало ручной реконструкции: проверка retrieval scores через node -e, отдельный режим AI_MOCK_RESPONSE=true для исключения LLM и изоляции retrieval-слоя. В результате выяснилось, что релевантный контекст не попадал в prompt на этапе сборки.
При наличии event log с причинной связностью контекст становится воспроизводимой проекцией состояния системы. Это переводит отладку из восстановления фактов в навигацию по correlation_id. Практический эффект - тест начинает проверять не только выход системы, но и входное состояние, в котором этот выход был сгенерирован.
Момент с "агент оказался умнее эталона"- самый точный в статье. В одном из моих RAG-тестов система вернула не ту специальность, которую ожидал тест. После разбора трейсов оказалось, что симптомы были клинически неоднозначными, и модель по сути была права. В итоге фиксили не агента, а тестовые данные.
Про ReDoS через множество синтаксических вариантов интересно именно с точки зрения тестирования.
Markdown-парсер может выглядеть полностью стабильным на обычных входных данных и разваливаться на специально сконструированной строке. Обычные happy-path тесты такое не поймают, тут уже нужен fuzzing или property-based testing.
Для того, что большинство воспринимает как "просто форматирование текста", поверхность для ошибок и атак неожиданно глубокая.
Согласна, E2E подразумевает полный стек. Я скорее о том, что если API тест уже проверяет статус-код и контракт, то дублировать это в UI тесте смысла нет.
В моей схеме UI тест всё равно работает с реальным бэкендом, просто проверяет то, что видит пользователь: появился ли нужный текст, сменилось ли состояние формы.
Согласна с предыдущим комментатором - тесты это не черновая работа. Добавлю конкретный пример: писала тест на demographic neutrality для медицинской системы, AI сгенерировал набор: “25-летний мужчина → Cardiologist”, “70-летняя женщина → Cardiologist”. Код чистый, логика понятная. Проблему нашла при ревью: “А ребёнок?” Для Pediatrician демография должна влиять на рекомендацию - это клинически корректное поведение. AI не знал этой границы, тест верифицировал неправильную вещь.
Это не про качество кода (тут как раз всё было аккуратно). Это про то, что без понимания домена тест может быть синтаксически идеальным и семантически неверным. В моём случае это создало иллюзию покрытия там, где его не было.
Знакомая проблема с evaluation - LLM-оценщик предпочитает уверенно звучащие ответы, не правильные. Решила обойти это: не оцениваю качество ответа, тестирую инварианты.
Два конкретных: (1) specialty в ответе должна быть из ALLOWED_SPECIALTIES - детерминированная проверка, не зависит от модели-оценщика; (2) одно клиническое описание в пяти перефразировках должно возвращать одну и ту же specialty - если нет, модель реагирует на формулировку, не на смысл.
Плюс distribution drift: есть baseline с ожидаемым распределением specialty по корпусу запросов, scheduled pipeline сравнивает реальное с baseline. Не ловит неправильный ответ на конкретный запрос, но ловит смещение поведения модели со временем.
Tunnel vision есть и в автоматизации тестирования но выглядит немного иначе: когда пишешь тесты по тому же спеку который тестируешь, кодируешь те же слепые пятна что и спек. Второй тестировщик это лечит, но не всегда есть команда.
В своём проекте столкнулась с похожим: IDOR на GET /appointments/:id не поймали API тесты написанные по контракту - нашли отдельно, при security ревью. Контракт описывал что вернуть, но не говорил кому можно смотреть чужую запись. Тест воспроизвёл то же слепое пятно.
Peer testing и security audit решают одну задачу разными инструментами - добавить взгляд который не знает как система должна работать.
Столкнулась с вариантом в WebdriverIO: написала if (items.length === 0) throw new Error(…). TypeScript молчал, тест никогда не падал даже на пустом экране.
Причина: .length на ChainablePromiseArray — это Promise, не number. Promise === 0 всегда false. Проверка тихо не работала - ни ошибки, ни предупреждения.
Интересно, что принцип "короткой памяти" здесь фактически заменяет историю диалога текущим состоянием системы. История чата накапливает не только полезный контекст, но и шум. Код в этом смысле выступает как источник истины о том, что система представляет собой сейчас.
Но есть и обратная сторона. Код хорошо хранит состояние системы, но гораздо хуже хранит контекст принятия решений. По коду обычно видно, что сделано, но не всегда понятно, почему именно так.
Для автономных модулей такой подход выглядит очень практичным. Для долгоживущих систем это наверное открытый вопрос - где хранить не только текущее состояние, но и reasoning, который к нему привёл.
Самый интересный технический вывод здесь для меня это не метафоры, а промпты-абракадабра. Если эволюционно отобранная бессмыслица даёт сопоставимый прирост точности, то семантическое содержание системного промпта, по крайней мере в этих экспериментах, не выглядит необходимым условием. В статье предполагается влияние структуры и распределения токенов но это пока наблюдение по поведению black box, не объяснение механизма.
Ещё важное различие, которое в статье не очень акцентировано: абракадабра повышает точность, но для behavioural steering уже нужен связный нарратив. Похоже, два разных эффекта с разными механизмами.
С точки зрения безопасности, если управление поведением прячется в безобидной метафоре, традиционные лексические фильтры оказываются не слишком надёжным инструментом обнаружения.
Согласна. В статье я сфокусировалась на семантике soft delete и недостаточно раскрыла вопрос атомарности операции.
В реальной реализации такие изменения действительно должны выполняться в транзакции. Моя мысль была скорее о том, что даже при корректной технической реализации остаётся вопрос бизнес-семантики: что именно означает “удалить пользователя” для waitlist, appointment и других связанных компонентов.
Спасибо за комментарий.
Согласна, что показанный фикс решает только конкретный сценарий с waitlist и сам по себе не отвечает на вопрос, что делать с остальными связанными сущностями.
Наверное, моя формулировка про то, что “promoteFromWaitlist написан правильно” была слишком категоричной. Точнее было бы сказать, что функция была написана в соответствии со своими локальными правилами и не учитывала состояние deletedAt. Именно это расхождение между локальной логикой компонентов и привело к появлению ghost appointment.
Собственно, это и было основным наблюдением из инцидента: soft delete существовал на уровне users, но не был частью общего контракта системы.
Согласна, транзитивное покрытие действительно закрывает кейс со screenshot-тестами, пример был неудачным. Если изменение во View C, тест для View A через execution path подтянется.
Coverage и impact graph отвечают на разные вопросы.
Coverage-based selection отвечает: какие существующие тесты исполняли изменённый код. Impact analysis отвечает: какие компоненты зависят от изменения и есть ли для них тесты вообще.
Для запуска тестов coverage часто оказывается сильной эвристикой. Но граф ещё показывает пробелы: в статье отчёт явно говорит "ButtonBarBase без screenshot-теста" - если теста нет, coverage его не найдёт, нечего выбирать.
Скорее вижу эти подходы как дополняющие, а не конкурирующие.
Из опыта тестирования roguelike-движка: несоответствие механики и контекста часто не видно на этапе проектирования - оно проявляется только в прогонe игры и поведении системы. Механика случайного урона задумывалась как источник вариативности. Но анализ прогонов показал: в сценариях с одним врагом она почти не влияет на исход - RNG фактически "схлопывается".
Похожая история была с комбинацией bleed + defend - она не проектировалась как доминирующая стратегия, но в данных оказалась устойчивым паттерном, который усиливает сам себя. Поэтому "не конфликтуют" и "усиливают друг друга" - разные уровни проверки. Первое видно в дизайне, второе только в данных прогонов.
Подход с пересечением coverage и diff хорошо работает для тестов, где есть прямая связь "код → поведение" (unit / integration логика). Но у него появляется слепая зона, если перенести его на дизайн-систему и screenshot/visual regression тесты. В таких тестах покрытие строк кода вообще не коррелирует с тем, что мы проверяем. ButtonBase может покрывать десятки строк, но тест утверждает не выполнение кода, а визуальный контракт компонента.
Если ButtonCore меняет отступы, тени или layout-логику - coverage и diff покажут "затронуто", но не дадут ответа, какие визуальные контракты сломались. А именно это и есть цель теста.
Граф зависимостей из статьи решает другую задачу - не "какие тесты запускать по строкам", а "какие контракты системы потенциально затронуты изменением". В UI-слое coverage часто не является хорошей прокси для риска.
Хочу добавить практический кейс, который попадает в серую зону между stub и fake (и частично mock, если смотреть на поведение).
В моем проекте AI-эндпоинт POST /ai/recommend-doctor работает с флагом AI_MOCK_RESPONSE=true. В этом режиме внешний LLM (Claude) отключается, но retrieval layer и БД продолжают работать полностью в реальном режиме. Меняется только финальная генерация ответа. По сути это не чистый stub и не fake - это гибрид: часть системы настоящая, часть заменена, но поведение остаётся близким к production pipeline.
Интересный эффект проявился в CI. Тест ожидал Pediatrician, но получил General Practitioner. Claude в этом режиме не вызывался, значит проблема была не в LLM, а в retrieval layer.
В production этот баг был бы легко "замаскирован": LLM мог бы скорректировать неправильный top-1 и тест прошёл бы зелёным. Mock mode убрал этот компенсирующий слой и показал реальную проблему ранжирования.
Получается, что частично мокнутая система иногда даёт более честный сигнал, чем полностью интегрированная - потому что убирает "умный буфер", который скрывает ошибки upstream.
Самый полезный вопрос для ревью AI-сгенерированного теста у меня обычно не "какое покрытие он дал?", а "какой баг он должен поймать?".
Столкнулась с этим на сценарии для экрана AI-рекомендаций врачей. Генератор предлагал проверки в духе "результаты отображаются" и "навигация работает". Формально всё зелёное. Но реальное требование было другим: пользователь должен явно видеть способ обойти рекомендацию и перейти к полному списку врачей. Фича существовала технически, но была плохо обнаружима в контексте экрана.
Тест, который в итоге оказался полезным, проверял именно наличие видимой кнопки browse-all-doctors в результатах рекомендации. Большинство автоматически сгенерированных вариантов до такого требования не доходили, потому что воспроизводили текущую реализацию, а не бизнес-ожидание.
Публичных материалов практически нет, много концептуального про non-deterministic testing, но очень мало конкретных практик с кодом.
Из своего: разделила AI-тесты на два условных слоя: contractLayer (domain invariants + hallucination detection, работает с моком без реального Claude) и qualityLayer (metamorphic consistency + statistical assertions, только с реальной моделью). Технически 2 describe с разными условиями запуска. Это для одного AI-эндпоинта. Для полной оркестратор-воркер цепочки coverage будет другим. Складывалось по частям, не как система сразу. Этот gap между “концепция” и “вот рабочий код” - то, что хочу со временем закрыть в своём контенте.
Ваш пример страшнее :) тест был правильным, пока агент его не "починил".
Интересный разрез про latency и chunking. В RAG есть более базовый слой - это наблюдаемость retrieval. На практике ключевая проблема не в том, "что вернул поиск", а в том, какой именно контекст оказался в prompt на конкретном шаге. При эвристическом ранжировании это часто неочевидно и плохо воспроизводимо.
В одном RAG-пайплайне тест падал с 422 и первопричина оказалась в retrieval-слое: релевантный контекст не попадал в LLM из-за логики матчинг/скоринга. На уровне симптомов это выглядело как ошибка модели, но LLM просто работал с некорректным входом. После изоляции retrieval через mock-режим стало видно, что проблема находится до генерации.
В этом смысле top-k, chunk size и retrieval strategy - это не только параметры качества и latency, а границы наблюдаемости: они определяют, можно ли вообще восстановить и зафиксировать вход системы в момент ответа.
Самое ценное в такой архитектуре с точки зрения тестируемости это связка causation_id + immutable log.
У меня в RAG-пайплайне был кейс, где тест падал с 422 (неправильная специализация). Проблема была не в модели, а в том что retrieval layer формировал некорректный контекст.
Ключевой вопрос в отладке был не "ошибся ли LLM", а "какие именно данные попали в контекст на этом шаге". Без иммутабельного лога это требовало ручной реконструкции: проверка retrieval scores через node -e, отдельный режим AI_MOCK_RESPONSE=true для исключения LLM и изоляции retrieval-слоя. В результате выяснилось, что релевантный контекст не попадал в prompt на этапе сборки.
При наличии event log с причинной связностью контекст становится воспроизводимой проекцией состояния системы. Это переводит отладку из восстановления фактов в навигацию по correlation_id. Практический эффект - тест начинает проверять не только выход системы, но и входное состояние, в котором этот выход был сгенерирован.
Момент с "агент оказался умнее эталона"- самый точный в статье. В одном из моих RAG-тестов система вернула не ту специальность, которую ожидал тест. После разбора трейсов оказалось, что симптомы были клинически неоднозначными, и модель по сути была права. В итоге фиксили не агента, а тестовые данные.
Про ReDoS через множество синтаксических вариантов интересно именно с точки зрения тестирования.
Markdown-парсер может выглядеть полностью стабильным на обычных входных данных и разваливаться на специально сконструированной строке. Обычные happy-path тесты такое не поймают, тут уже нужен fuzzing или property-based testing.
Для того, что большинство воспринимает как "просто форматирование текста", поверхность для ошибок и атак неожиданно глубокая.
Согласна, E2E подразумевает полный стек. Я скорее о том, что если API тест уже проверяет статус-код и контракт, то дублировать это в UI тесте смысла нет.
В моей схеме UI тест всё равно работает с реальным бэкендом, просто проверяет то, что видит пользователь: появился ли нужный текст, сменилось ли состояние формы.
Согласна с предыдущим комментатором - тесты это не черновая работа. Добавлю конкретный пример: писала тест на demographic neutrality для медицинской системы, AI сгенерировал набор: “25-летний мужчина → Cardiologist”, “70-летняя женщина → Cardiologist”. Код чистый, логика понятная. Проблему нашла при ревью: “А ребёнок?” Для Pediatrician демография должна влиять на рекомендацию - это клинически корректное поведение. AI не знал этой границы, тест верифицировал неправильную вещь.
Это не про качество кода (тут как раз всё было аккуратно). Это про то, что без понимания домена тест может быть синтаксически идеальным и семантически неверным. В моём случае это создало иллюзию покрытия там, где его не было.
Знакомая проблема с evaluation - LLM-оценщик предпочитает уверенно звучащие ответы, не правильные. Решила обойти это: не оцениваю качество ответа, тестирую инварианты.
Два конкретных: (1) specialty в ответе должна быть из ALLOWED_SPECIALTIES - детерминированная проверка, не зависит от модели-оценщика; (2) одно клиническое описание в пяти перефразировках должно возвращать одну и ту же specialty - если нет, модель реагирует на формулировку, не на смысл.
Плюс distribution drift: есть baseline с ожидаемым распределением specialty по корпусу запросов, scheduled pipeline сравнивает реальное с baseline. Не ловит неправильный ответ на конкретный запрос, но ловит смещение поведения модели со временем.
Tunnel vision есть и в автоматизации тестирования но выглядит немного иначе: когда пишешь тесты по тому же спеку который тестируешь, кодируешь те же слепые пятна что и спек. Второй тестировщик это лечит, но не всегда есть команда.
В своём проекте столкнулась с похожим: IDOR на GET /appointments/:id не поймали API тесты написанные по контракту - нашли отдельно, при security ревью. Контракт описывал что вернуть, но не говорил кому можно смотреть чужую запись. Тест воспроизвёл то же слепое пятно.
Peer testing и security audit решают одну задачу разными инструментами - добавить взгляд который не знает как система должна работать.
Столкнулась с вариантом в WebdriverIO: написала if (items.length === 0) throw new Error(…). TypeScript молчал, тест никогда не падал даже на пустом экране.
Причина: .length на ChainablePromiseArray — это Promise, не number. Promise === 0 всегда false. Проверка тихо не работала - ни ошибки, ни предупреждения.