← Все кейсы
Развлечения Разработческий харнес

PARTYstation: разработческий харнес для живой legacy-платформы

Мы строим разработческий харнес для живой игровой платформы PARTYstation на PHP и Node: обкладываем работающую систему тестами, картами и правилами так, чтобы её переписывала и поддерживала пара человек с агентами, ничего не ломая в продакшене. Цель — покрыть харнесом всю систему, а не переписать её целиком.

Живой продукт, который нельзя останавливать

PARTYstation — это платформа для игр на вечеринках. Игра идёт на большом экране, на телевизоре дома, на дне рождения или на корпоративе, а игроки подключаются со своих телефонов как с пультов: квизы, «Крокодил», ассоциации, мемные раунды. Десяток типов игр, до нескольких десятков игроков в одной комнате, веб-ТВ, мобильные приложения, Android TV, Samsung. Те же игры PARTYstation встроены и внутрь стриминговых сервисов, Кинопоиска и Wink, где их запускают прямо с телевизора. Зарабатывает платформа подписками и платными играми, поэтому каждая оборванная игра бьёт по выручке напрямую: гость не доиграл и отменил подписку.

PARTYstation: квиз на большом экране, телефоны в руках игроков работают пультами
Игра идёт на большом экране, телефоны работают пультами

К моменту, когда мы подключились, платформа прожила в продакшене несколько лет. Бэкенд за это время разъехался на два языка: REST API на PHP и игровые серверы на Node.js, которые держат вебсокет-соединение с каждым телефоном в комнате. Сверху — годы накопленного legacy: код, который уже никто целиком не держит в голове, интеграции, оставленные «на всякий случай», и поведение, которое работает, но никто не помнит почему. Технический долг копился быстрее, чем выходили новые фичи, и большой командой это перестало разгребаться.

Чего стоит такой legacy, видно по новогоднему инциденту. Итоги турниров на самом деле лежат в базе, но игровой сервер читал их только из быстрого кэша в Redis и не умел сходить за ними в базу, если кэша вдруг нет. В новогоднюю ночь общий Redis перезапустился прямо во время турнира, кэш с результатами опустел, и игроки не увидели итогов, пока кэш не наполнился заново. Данные были целы всё это время, просто система не умела их достать.

Деплой при этом выполняют руками: shell-скрипт заходит в каждый репозиторий, делает git reset --hard, тянет master и перезапускает процессы. Наблюдаемости на уровне приложений нет, разбор аварии идёт через SSH и просмотр логов вручную.

Компания решила не возвращать большую команду. Вместо этого она меняет саму модель разработки: оставляет небольшую группу инженеров и ведёт продукт через кодовых агентов. Ставка простая — пара человек с агентами должна держать и развивать систему, которую раньше тянула целая команда.

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

Спекой становится сам продукт

Спецификацией мы назначили само текущее поведение системы. Документ на рефакторинг продуктовая компания вычитывать и подписывать не станет: у неё нет времени валидировать то, что выглядит как внутреннее переустройство. Эталон — это код на их боевой ветке и то, как он отвечает прямо сейчас.

Переписанная версия обязана пройти тот же регресс, что и оригинал, и это и есть критерий приёмки. Документ, который устарел бы на следующем релизе, заменили на проверку, которую можно прогнать в любой момент.

Весь процесс — четыре шага

Из этого решения вырос весь метод. Он умещается в четыре шага, и каждый следующий опирается на предыдущий. Дальше разберём их по одному.

01
Снять карту легаси
Архитектурная карта по 11 репозиториям. Чужой код агент читает только через изолированного подагента-археолога. Найденные баги ложатся в реестр.
понять чужое
02
Зафиксировать поведение тестами
Один набор тестов на двух стендах, старом и новом. Мультиплеер из большого экрана и телефонов игроков в одной комнате. Расхождение между стендами — это ошибка.
эталон
03
Переписать с паритетом
Поведение один в один, вплоть до сайд-эффектов. Баги легаси переносим намеренно, чиним только дешёвые и безопасные.
Python
04
Выкатить по одной ручке
nginx шлёт переписанные адреса в новый сервис, остальное — в старый PHP. Откат — одна строка конфига.
без «взрыва»

Сначала тест, потом строчка нового кода

Если спека — это поведение, то фиксировать его нужно раньше, чем трогаешь код. Один и тот же набор тестов мы прогоняем против двух стендов: старого на PHP и Node и нового на Python. Тест не знает, на какой системе он сейчас работает, а ветки вида «если это новый стенд, проверяем иначе» запрещены: именно расхождение в поведении между стендами и есть сигнал об ошибке.

Сначала тот же набор прогоняется на их действующей, ещё не переписанной системе. Зелёный прогон там подтверждает, что тест описывает реальное поведение, и только после этого он становится эталоном для нового кода.

Проверяем лесенкой уровней, от мелкого к крупному:

Самый сложный уровень — игра в реальном времени на несколько человек. Чтобы проверить один сценарий, тест поднимает сразу несколько изолированных браузеров: большой экран, на котором идёт игра, и телефоны игроков, причём ведущий тоже подключается с телефона. У каждого свои куки, и все сводятся в одну комнату через общий пул игровых серверов по вебсокету.

Дальше харнес проигрывает реальный протокол игры — выбрать игру, ответить на вопрос, перейти к следующему раунду, поставить паузу — и сверяет состояние комнаты, которое рассылается всем участникам. Заглушек-моков, которые подменяют настоящие части системы фейковыми, здесь нет нигде. Единственное, что нам позволено менять в самом продукте, — это добавлять элементам интерфейса разметку-идентификаторы, чтобы за них могли зацепиться тесты.

Паритет снаружи, чистая архитектура внутри

Здесь важно различать две вещи: что система делает наружу и как она устроена внутри. Наружу — это ответ ручки, её статус и побочные эффекты, на которые завязаны другие части системы. Это контракт, и его мы держим один в один. А как всё сделано внутри, переписываем начисто и архитектурные глупости в новый код не тащим.

Поэтому паритет — это не только совпадающий ответ эндпоинта (отдельного адреса API). У старого кода есть побочные эффекты, на которые кто-то уже полагается: записи в базу, строки в аудит-логе, обращения к соседним сервисам, сброс кэша. Верный порт повторяет и их. Логин администратора, например, пишет строку в таблицу логов с конкретной категорией, уровнем и текстом сообщения — такие детали мы выписываем для каждого эндпоинта под заголовком «сайд-эффекты», иначе их молча теряет любой наивно переписанный код.

Из той же логики растёт самое контринтуитивное решение — про баги. Баг в бизнес-логике, на который уже завязались мобильные клиенты, админ-прокси и внутренние джобы, мы переносим как есть: тихо исправить его означает сломать тех, кто полагался на старую форму. А внутреннюю глупость вроде десятков лишних запросов в базу на каждое действие игрока мы не воспроизводим, потому что снаружи её никто не видит и переписать её начисто безопасно.

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

Дефект в legacyРешениеПочему так
Служебная ручка отдаёт данные игроков без проверки доступаПеренести как есть, фикс — отдельным планомЗакрыть доступ — значит сменить контракт для всех, кто её зовёт: админ-прокси, внутренние джобы. Риск выше порога
На каждое действие игрока — десятки лишних запросов в БД за ачивкамиПереписать начистоЭто внутренний костыль, снаружи его не видно. Такой веер запросов в новом коде воспроизводить нельзя
Ответ с неизвестным content-type возвращает 200 вместо ошибкиОставить как естьСтарое мобильное приложение завязано на текущий код ошибки, поэтому правка сломала бы живых клиентов

Как научить агента работать в чужом коде

Главная ловушка агентской разработки на legacy всплыла рано: когда агент читает старый PHP напрямую, он начинает писать на Python в стиле PHP и тянет в новый код чужую структуру вместо аккуратных слоёв. Поэтому доступ к legacy мы изолировали.

Единственный способ для основного агента заглянуть в старый код — это отдельный подагент-археолог. Он работает только на чтение, ему передают точный путь к репозиторию (угадывать и шарить по диску ему запрещено), и возвращает он короткую выжимку со ссылками на конкретные строки и обязательным разбором сайд-эффектов; сами файлы в основной контекст не попадают. Так контекст основного агента остаётся чистым, а архитектура нового кода не дрейфует в сторону legacy.

Дальше идут правила, по которым агент пишет уже наш код. Для каждого сервиса есть своя инструкция о слоях с жёсткой таблицей решений и одним честным выходом: если изменение не ложится в слои, агент обязан остановиться и спросить, а не выдумывать новый слой.

Эти же инструкции гасят типичную манеру моделей переусложнять — никаких абстрактных интерфейсов там, где реализация всего одна, никаких репозиториев на каждую сущность «на будущее». Отдельные правила требуют падать сразу при отсутствии настройки, а не подставлять молчаливый дефолт, который потом всплывёт багом.

Перенос по одной ручке за раз

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

Запрос приходит наКуда уходит сейчас
Переписанные ручки — сейчас это авторизацияНовый сервис на Python
Всё остальноеСтарый PHP, как и раньше

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

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

Следующий крупный кусок — игровой сервер, тот самый на вебсокетах: в legacy это примерно 32 тысячи строк, десяток машин состояний (по сути отдельный движок на каждый тип игры, который ведёт партию по её правилам) и больше восьми десятков типов сообщений в обе стороны. Сейчас он в работе: каркас, маршрутизация и проверка, что Redis не теряет данные при перезапуске (в ту новогоднюю ночь именно из-за неё игроки и не увидели итогов турнира), уже на Python; по оценке команды это около пятой части сервера.

Что под капотом — как устроен харнес
ЖИВОЙ LEGACY
11 репозиториев · REST API на PHP · игровые серверы на Node.js · веб-ТВ, мобильные, Android TV, Samsung · единый Postgres, Redis, деплой shell-скриптами
два параллельных контура
КАРТА: ПОНЯТЬ, ЧТО ЕСТЬ
собрать общую картину чужой системы до первой строки нового кода
Архитектурная карта legacy нотация C4
по 11 репозиториям: контекст · контейнеры · компоненты, плюс read-only инспекция прода
Подагент-археолог только чтение
путь к репозиторию передают извне, угадывать запрещено
возвращает выжимку со ссылками на строки и разбором сайд-эффектов
Реестр legacy-багов едет с кодом
статусы и решение по каждому: перенести как есть, отклониться или починить
фикс — только если риск правки ниже порога
СПЕКА В ТЕСТАХ: ЗАФИКСИРОВАТЬ ПОВЕДЕНИЕ
критерием приёмки становится само поведение системы
Один набор на двух стендах старый и новый
тест не знает, где работает; расхождение поведения — это ошибка
сначала прогон на действующей системе — проверка, что тест верен
Мультиплеер в браузерах вебсокет
ведущий на веб-ТВ плюс телефоны-пульты в отдельных контекстах в одной комнате
без моков; в продукте меняем только разметку-идентификаторы
Карта покрытия 1032 сценария / 15 доменов
единый источник истины по сценариям и план работ
единый эталон поведения →
карта системы реестр багов тесты на двух стендах
ПОРТ НА PYTHON паритет вплоть до сайд-эффектов
монорепо на uv · FastAPI + Pydantic v2 · async SQLAlchemy
миграции сделаны безопасными для повторного прогона, чтобы ужиться с живыми миграциями Laravel на одном Postgres
контракты — единый источник формы сообщений
snake_case в коде, camelCase на проводе; отсюда же родятся типы для фронта
слои по сервисам плюс правила для агента
падать при отсутствии настройки вместо молчаливых дефолтов, без переусложнения «на будущее»
ВЫКАТ по одному эндпоинту
точечные правила nginx отправляют переписанные адреса в новый контейнер, остальное — в старый PHP
откат — убрать строку маршрута в конфиге, никакого «большого взрыва»
авторизация работает на рабочем контуре рядом с боевой системой · игровой сервер: каркас и маршрутизация на Python (~пятая часть) · 1032 сценария, автоматизирована малая часть
Python · FastAPI · Pydantic v2 · SQLAlchemy async · uv-монорепо · подагент-археолог · реестр багов · карта покрытия
агентская оснастка остаётся у разработчиков, в продукт заказчику не входит

Что меняется и где мы сейчас

Главная цель проекта — покрыть систему целиком разработческим харнесом: оснасткой, через которую любую задачу по ней можно вести агентом при минимальном участии человека. Само переписывание на Python вторично. Перепишется столько, сколько успеем, а харнес должен накрыть всё, включая то, что мы трогать не будем.

Поэтому и итог первой части измеряется не строчками переписанного кода. Работу, которую раньше тянула целая команда, теперь держит пара человек с агентами, и legacy при этом остаётся в продакшене целым.

Кусок системыСостояние
Карта легаси, реестр багов, агентский харнесГотово
Авторизация — 10 ручекПереписана, работает на рабочем контуре
Игровой серверВ работе, примерно пятая часть
Карта покрытия — 1032 сценария / 15 доменовАвтоматизируется, пока малая часть
Остальные клиенты, наблюдаемостьДальше по плану

Сама сделка устроена честно для незавершённой работы: сроки и охват системы харнесом зафиксированы, а глубина переписывания остаётся переменной — настолько, насколько успеваем, начиная с самых тяжёлых кусков legacy. Порядок везде один: сначала тест, потом строчка нового кода.

Что мы вынесли из пилота

Legacy как исполнимая спека вместо документации на согласование
Один набор тестов проходит и на старом, и на новом коде
Паритет вплоть до сайд-эффектов: записи в БД, аудит-логи, инвалидации кэша
Баги легаси переносим намеренно — реестр с порогом риска на фикс
Архитектурная карта 11 репозиториев до первой строки нового кода
Агент трогает чужой код только через изолированный подагент-археолог
Авторизация уже переписана и работает через nginx-маршрутизацию
Legacy как исполнимая спека вместо документации на согласование
Один набор тестов проходит и на старом, и на новом коде
Паритет вплоть до сайд-эффектов: записи в БД, аудит-логи, инвалидации кэша
Баги легаси переносим намеренно — реестр с порогом риска на фикс
Архитектурная карта 11 репозиториев до первой строки нового кода
Агент трогает чужой код только через изолированный подагент-археолог
Авторизация уже переписана и работает через nginx-маршрутизацию

Расскажите, какой процесс хотите разобрать.

Ответим, подходит ли задача для AI-агентов, и если да, предложим конкретный план.

или напишите напрямую — ilya@manaraga.ai