Мы строим разработческий харнес для живой игровой платформы PARTYstation на PHP и Node: обкладываем работающую систему тестами, картами и правилами так, чтобы её переписывала и поддерживала пара человек с агентами, ничего не ломая в продакшене. Цель — покрыть харнесом всю систему, а не переписать её целиком.
PARTYstation — это платформа для игр на вечеринках. Игра идёт на большом экране, на телевизоре дома, на дне рождения или на корпоративе, а игроки подключаются со своих телефонов как с пультов: квизы, «Крокодил», ассоциации, мемные раунды. Десяток типов игр, до нескольких десятков игроков в одной комнате, веб-ТВ, мобильные приложения, Android TV, Samsung. Те же игры PARTYstation встроены и внутрь стриминговых сервисов, Кинопоиска и Wink, где их запускают прямо с телевизора. Зарабатывает платформа подписками и платными играми, поэтому каждая оборванная игра бьёт по выручке напрямую: гость не доиграл и отменил подписку.
К моменту, когда мы подключились, платформа прожила в продакшене несколько лет. Бэкенд за это время разъехался на два языка: REST API на PHP и игровые серверы на Node.js, которые держат вебсокет-соединение с каждым телефоном в комнате. Сверху — годы накопленного legacy: код, который уже никто целиком не держит в голове, интеграции, оставленные «на всякий случай», и поведение, которое работает, но никто не помнит почему. Технический долг копился быстрее, чем выходили новые фичи, и большой командой это перестало разгребаться.
Чего стоит такой legacy, видно по новогоднему инциденту. Итоги турниров на самом деле лежат в базе, но игровой сервер читал их только из быстрого кэша в Redis и не умел сходить за ними в базу, если кэша вдруг нет. В новогоднюю ночь общий Redis перезапустился прямо во время турнира, кэш с результатами опустел, и игроки не увидели итогов, пока кэш не наполнился заново. Данные были целы всё это время, просто система не умела их достать.
Деплой при этом выполняют руками: shell-скрипт заходит в каждый репозиторий, делает git reset --hard, тянет master и перезапускает процессы. Наблюдаемости на уровне приложений нет, разбор аварии идёт через SSH и просмотр логов вручную.
Компания решила не возвращать большую команду. Вместо этого она меняет саму модель разработки: оставляет небольшую группу инженеров и ведёт продукт через кодовых агентов. Ставка простая — пара человек с агентами должна держать и развивать систему, которую раньше тянула целая команда.
Наша задача — построить то, что делает такую модель возможной. Внутри мы называем это разработческим харнесом: оснасткой вокруг кода, благодаря которой агент понимает чужую систему, не вредит ей и доводит задачу до конца с минимальным участием человека.
Спецификацией мы назначили само текущее поведение системы. Документ на рефакторинг продуктовая компания вычитывать и подписывать не станет: у неё нет времени валидировать то, что выглядит как внутреннее переустройство. Эталон — это код на их боевой ветке и то, как он отвечает прямо сейчас.
Переписанная версия обязана пройти тот же регресс, что и оригинал, и это и есть критерий приёмки. Документ, который устарел бы на следующем релизе, заменили на проверку, которую можно прогнать в любой момент.
Из этого решения вырос весь метод. Он умещается в четыре шага, и каждый следующий опирается на предыдущий. Дальше разберём их по одному.
Если спека — это поведение, то фиксировать его нужно раньше, чем трогаешь код. Один и тот же набор тестов мы прогоняем против двух стендов: старого на PHP и Node и нового на Python. Тест не знает, на какой системе он сейчас работает, а ветки вида «если это новый стенд, проверяем иначе» запрещены: именно расхождение в поведении между стендами и есть сигнал об ошибке.
Сначала тот же набор прогоняется на их действующей, ещё не переписанной системе. Зелёный прогон там подтверждает, что тест описывает реальное поведение, и только после этого он становится эталоном для нового кода.
Проверяем лесенкой уровней, от мелкого к крупному:
Самый сложный уровень — игра в реальном времени на несколько человек. Чтобы проверить один сценарий, тест поднимает сразу несколько изолированных браузеров: большой экран, на котором идёт игра, и телефоны игроков, причём ведущий тоже подключается с телефона. У каждого свои куки, и все сводятся в одну комнату через общий пул игровых серверов по вебсокету.
Дальше харнес проигрывает реальный протокол игры — выбрать игру, ответить на вопрос, перейти к следующему раунду, поставить паузу — и сверяет состояние комнаты, которое рассылается всем участникам. Заглушек-моков, которые подменяют настоящие части системы фейковыми, здесь нет нигде. Единственное, что нам позволено менять в самом продукте, — это добавлять элементам интерфейса разметку-идентификаторы, чтобы за них могли зацепиться тесты.
Здесь важно различать две вещи: что система делает наружу и как она устроена внутри. Наружу — это ответ ручки, её статус и побочные эффекты, на которые завязаны другие части системы. Это контракт, и его мы держим один в один. А как всё сделано внутри, переписываем начисто и архитектурные глупости в новый код не тащим.
Поэтому паритет — это не только совпадающий ответ эндпоинта (отдельного адреса API). У старого кода есть побочные эффекты, на которые кто-то уже полагается: записи в базу, строки в аудит-логе, обращения к соседним сервисам, сброс кэша. Верный порт повторяет и их. Логин администратора, например, пишет строку в таблицу логов с конкретной категорией, уровнем и текстом сообщения — такие детали мы выписываем для каждого эндпоинта под заголовком «сайд-эффекты», иначе их молча теряет любой наивно переписанный код.
Из той же логики растёт самое контринтуитивное решение — про баги. Баг в бизнес-логике, на который уже завязались мобильные клиенты, админ-прокси и внутренние джобы, мы переносим как есть: тихо исправить его означает сломать тех, кто полагался на старую форму. А внутреннюю глупость вроде десятков лишних запросов в базу на каждое действие игрока мы не воспроизводим, потому что снаружи её никто не видит и переписать её начисто безопасно.
Каждый найденный дефект попадает в реестр, который едет вместе с кодом, и по каждому принимается отдельное решение: перенести поведение или переписать начисто. По-настоящему чиним только там, где правка дешёвая и безопасная, риск порядка десяти-пятнадцати процентов и ниже.
| Дефект в legacy | Решение | Почему так |
|---|---|---|
| Служебная ручка отдаёт данные игроков без проверки доступа | Перенести как есть, фикс — отдельным планом | Закрыть доступ — значит сменить контракт для всех, кто её зовёт: админ-прокси, внутренние джобы. Риск выше порога |
| На каждое действие игрока — десятки лишних запросов в БД за ачивками | Переписать начисто | Это внутренний костыль, снаружи его не видно. Такой веер запросов в новом коде воспроизводить нельзя |
| Ответ с неизвестным content-type возвращает 200 вместо ошибки | Оставить как есть | Старое мобильное приложение завязано на текущий код ошибки, поэтому правка сломала бы живых клиентов |
Главная ловушка агентской разработки на legacy всплыла рано: когда агент читает старый PHP напрямую, он начинает писать на Python в стиле PHP и тянет в новый код чужую структуру вместо аккуратных слоёв. Поэтому доступ к legacy мы изолировали.
Единственный способ для основного агента заглянуть в старый код — это отдельный подагент-археолог. Он работает только на чтение, ему передают точный путь к репозиторию (угадывать и шарить по диску ему запрещено), и возвращает он короткую выжимку со ссылками на конкретные строки и обязательным разбором сайд-эффектов; сами файлы в основной контекст не попадают. Так контекст основного агента остаётся чистым, а архитектура нового кода не дрейфует в сторону legacy.
Дальше идут правила, по которым агент пишет уже наш код. Для каждого сервиса есть своя инструкция о слоях с жёсткой таблицей решений и одним честным выходом: если изменение не ложится в слои, агент обязан остановиться и спросить, а не выдумывать новый слой.
Эти же инструкции гасят типичную манеру моделей переусложнять — никаких абстрактных интерфейсов там, где реализация всего одна, никаких репозиториев на каждую сущность «на будущее». Отдельные правила требуют падать сразу при отсутствии настройки, а не подставлять молчаливый дефолт, который потом всплывёт багом.
Выкат устроен так, чтобы в любой момент можно было откатиться, убрав одну строку в конфиге. Локально поднимаются два стенда, старый и новый, поначалу одинаковые. На новом добавляется контейнер с переписанным сервисом, а nginx, веб-сервер на входе, решает, какой запрос куда направить.
| Запрос приходит на | Куда уходит сейчас |
|---|---|
| Переписанные ручки — сейчас это авторизация | Новый сервис на Python |
| Всё остальное | Старый PHP, как и раньше |
Перенос идёт по одному эндпоинту, под защитой тестов, без «большого взрыва», когда всё переключают разом. Новый код работает на той же боевой базе данных, что и старая система, поэтому изменения её структуры приходится делать так, чтобы они уживались с тем, что в ту же базу продолжает писать legacy.
На сегодня через такую маршрутизацию уже работает авторизация: десять эндпоинтов для игроков и администраторов, переписанных на Python и покрытых тестами ещё до переписывания. Пока это рабочий контур рядом с боевой системой, поднятый в российском облаке заказчика, чтобы данные игроков не покидали его периметр; на сам продакшен новый код выкатывается тем же поэндпоинтным способом.
Следующий крупный кусок — игровой сервер, тот самый на вебсокетах: в legacy это примерно 32 тысячи строк, десяток машин состояний (по сути отдельный движок на каждый тип игры, который ведёт партию по её правилам) и больше восьми десятков типов сообщений в обе стороны. Сейчас он в работе: каркас, маршрутизация и проверка, что Redis не теряет данные при перезапуске (в ту новогоднюю ночь именно из-за неё игроки и не увидели итогов турнира), уже на Python; по оценке команды это около пятой части сервера.
Главная цель проекта — покрыть систему целиком разработческим харнесом: оснасткой, через которую любую задачу по ней можно вести агентом при минимальном участии человека. Само переписывание на Python вторично. Перепишется столько, сколько успеем, а харнес должен накрыть всё, включая то, что мы трогать не будем.
Поэтому и итог первой части измеряется не строчками переписанного кода. Работу, которую раньше тянула целая команда, теперь держит пара человек с агентами, и legacy при этом остаётся в продакшене целым.
| Кусок системы | Состояние |
|---|---|
| Карта легаси, реестр багов, агентский харнес | Готово |
| Авторизация — 10 ручек | Переписана, работает на рабочем контуре |
| Игровой сервер | В работе, примерно пятая часть |
| Карта покрытия — 1032 сценария / 15 доменов | Автоматизируется, пока малая часть |
| Остальные клиенты, наблюдаемость | Дальше по плану |
Сама сделка устроена честно для незавершённой работы: сроки и охват системы харнесом зафиксированы, а глубина переписывания остаётся переменной — настолько, насколько успеваем, начиная с самых тяжёлых кусков legacy. Порядок везде один: сначала тест, потом строчка нового кода.
Ответим, подходит ли задача для AI-агентов, и если да, предложим конкретный план.
Заявка отправлена
Ответим в течение дня на указанный email.
или напишите напрямую — ilya@manaraga.ai