Files
robotex/tests/harness.py
T
protokeyandClaude Opus 5.5 7b511d5d38 feature: архитектура v2 — api + очередь + worker, удержание окна браузера (#78)
- Сервис api (FastAPI, без Chrome): POST /v2/tasks, GET /v2/tasks/{id} (доступ по cookie
  сессии), DELETE /v2/sessions/current; /v1/solve — прежний контракт поверх очереди.
- Сервис worker (dramatiq + RabbitMQ): слоты браузера, окна сессий между задачами
  (browser_idle_timeout 0–120 мин, простой — TTL ключа в Redis), heartbeat.
- Маршрутизация задач сессии в персональную очередь воркера с открытым окном.
- Redis: задачи, результаты (RESULT_TTL_HOURS), окна, воркеры. Прокси и ключ 2captcha
  в Redis не сохраняются.
- Движок: AsyncStealthySession.run() — действия на текущей странице без навигации.
- Два образа (robotex-api, robotex-worker), compose с rabbitmq и redis,
  make up/down/logs/smoke, релиз собирает все образы.
- Тесты: api + worker в одном процессе (fakeredis), настоящий воркер dramatiq на
  StubBroker, браузерные тесты на новой схеме, smoke для запущенного стенда.
- Документация: README, API (v2), Deployment, Usage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:13:52 +04:00

66 lines
2.2 KiB
Python

"""api и worker в одном процессе без RabbitMQ и Redis: Redis — fakeredis, отправка в очередь подменена
доставкой сообщения прямо в WindowPool воркера (tests/unit, tests/browser)."""
import json
from contextlib import contextmanager
import fakeredis
from fastapi.testclient import TestClient
import main
from tasks import store as task_store
from tasks.broker import RUN_TASK
from worker import windows
WORKER_ID = "w1"
class Harness:
def __init__(self, store, pool, redis_sync):
self.store = store
self.pool = pool
self.redis = redis_sync
self.messages = []
self.outcomes = []
# True — сообщение выполняется сразу при отправке; False — копится в messages до deliver()
self.auto = True
self.client: TestClient | None = None
async def dispatch(self, message):
self.messages.append(message)
if self.auto:
await self.deliver(message)
async def deliver(self, message):
if message.actor_name == RUN_TASK:
self.outcomes.append(await self.pool.handle(*message.args))
else:
await self.pool.close(*message.args)
await self.store.heartbeat(WORKER_ID, self.pool.info())
def deliver_pending(self):
pending, self.messages = self.messages, []
for message in pending:
self.client.portal.call(self.deliver, message)
def heartbeat(self, **info):
self.redis.set(f"{task_store.PREFIX}:worker:{WORKER_ID}", json.dumps(self.pool.info() | info))
def call(self, fn, *args):
return self.client.portal.call(fn, *args)
@contextmanager
def run_env(monkeypatch, tmp_path, slots=2):
server = fakeredis.FakeServer()
store = task_store.TaskStore(fakeredis.aioredis.FakeRedis(server=server, decode_responses=True))
pool = windows.WindowPool(store, WORKER_ID, slots=slots)
harness = Harness(store, pool, fakeredis.FakeRedis(server=server, decode_responses=True))
monkeypatch.setattr(windows, "USER_DATA_ROOT", tmp_path)
monkeypatch.setattr(main, "store", store)
monkeypatch.setattr(main, "dispatch", harness.dispatch)
harness.heartbeat()
with TestClient(main.app) as client:
harness.client = client
yield harness
client.portal.call(pool.close_all)