# QA: серверная часть и ошибки подключения (live stand)

Дата: 2026-11-13 · Стенд: `https://calls.guardify.ru` · Контейнер:
`agil-calls-agil-calls-1` (image `agil-calls:0.1.0`, limit 768 MiB) · Node 23 /
TypeScript 5.9 · версия API `0.1.0`.

Артефакты (в `qa/backend/`):

| Файл | Что внутри |
|---|---|
| `api-sweep.json` | 91 проверка public/admin/validation эндпоинтов со статусами и латентностью |
| `connection-errors.json` | 17 сценариев ошибок подключения (WS/1008/room_full/kick/reconnect/…) |
| `load-ws-60.json`, `load-ws-60-run.log` | 60 участников по WS на стенде |
| `load-ws-100.json`, `load-ws-100-run.log` | 100 участников по WS на стенде |
| `live-60.log` | `live-room.mjs --users=60` на стенде до фикса: 10/60 (429) |
| `local-live-60.log` | тот же сценарий на исправленной локальной сборке: 60/60 |
| `local-server-logging.log` | проверка уровней логирования после фикса |
| `stand-diagnostics.json` | health/контейнер (CPU/RAM/OOM/restarts), наблюдаемые логи |
| `*.mjs` | воспроизводимые скрипты: `api-sweep`, `connection-errors`, `load-ws`, `cleanup-stand` |

Итог: **P0 нет.** Найден и исправлен **P1**, исправлен **P2**, два **P3**
зафиксированы (конфигурация стенда и ожидаемый rate-limit). P1 требует
пересборки образа и деплоя.

---

## 1. Нагрузка: много участников в звонке

| Сценарий | Результат | Детали |
|---|---|---|
| `live-room --users=60 --duration=600` (старый билд) | ⚠️ **10/60** | остальные `429 rate_limited` на `POST /join` — **P1**, исправлено |
| `live-room --users=60` на исправленной локальной сборке | ✅ **60/60** | `qa/backend/local-live-60.log` |
| 60 WS-участников на стенде (токены через `/v1/tokens`) | ✅ **60/60 welcome**, 0 ошибок | welcome avg 711 ms (p95 796), токены avg 167 ms |
| 100 WS-участников на стенде | ✅ **100/100 welcome**, 0 ошибок | welcome avg 779 ms (p95 977), токены avg 179 ms |
| Fan-out чат/субтитры/state при 60 | ✅ | chat 180 доставок, caption 177, state 177 |
| Fan-out при 100 | ✅ | chat 300, caption 297, state 297 |
| Контейнер под нагрузкой (60) | ✅ | CPU 6.75%, RAM 41.78 MiB / 768 MiB (5.4%), OOM=false, restarts=0 |
| Контейнер под нагрузкой (100) | ✅ | CPU 3.94%, RAM 42.01 MiB (5.5%) |
| Server health до/во время/после | ✅ 200 | rooms/uptime корректны, деградации нет |
| Логи сервера при нагрузке | ✅ | нет uncaught/crash/heap/ECONNRESET; ожидаемые 501/502 логировались как WARN после фикса (см. P2) |

Причина 10/60: join-rate-limit был **10/мин на (IP, комнату)** — при офисном
NAT/нагрузочном генераторе все 60 участников приходят с одного IP. Это не
проблема mesh/SFU, а неверный дефолт защиты. Исправление — см. P1.

Латентность welcome (~0.7–1.0 s) объясняется TLS+RTT до стенда (токены
~170 ms); на локальном сервере welcome p95 = 10 ms при 100 участниках,
RSS процесса 67–78 MB.

## 2. Ошибки подключения (WS) — 17/17 ✅

| Сценарий | Ожидание | Результат |
|---|---|---|
| WS с невалидным токеном | `error unauthorized` + close 1008 | ✅ (`code=1008`, `unauthorized`) |
| WS с JWT `alg:none` | отказ, error unauthorized | ✅ |
| Невалидные фреймы (битый JSON/неизвестный type) | `error bad_request`, соединение живо | ✅ |
| Flood 150 сообщений | `error rate_limited` + close 1008 | ✅, сервер healthy после |
| Повторный connect тем же токеном (replaced) | старый сокет закрыт `1000 replaced` | ✅ `(1000, replaced)` |
| Kick из admin API | `bye{kicked}` + close 1008 | ✅ kicked=200, close=1008 |
| Lock комнаты: join | `423 room_locked` | ✅ |
| Lock комнаты: WS | `error room_locked` + закрытие | ✅ (code=1008) |
| `room_full` (maxParticipants=2) | WS `room_full`, join `409` | ✅ |
| Reconnect ×3 с backoff | каждый раз welcome | ✅ 3/3 |
| Неверный путь WS (`/wsx`) | upgrade отклонён, без welcome | ✅ |
| Heartbeat (idle 3.5 s, протокольный ping) | соединение открыто | ✅ |
| Ping/pong сообщением | pong с тем же ts | ✅ |
| Здоровье сервера после всех сценариев | 200 | ✅ |
| Таймаут heartbeat/offline/expired/OIDC | — | не проверялись в этом прогоне (нет доступа к секрету токена/IdP); покрыты юнит-тестами WS |

## 3. Эндпоинты (свежий билд) — 91/91 ✅

`qa/backend/api-sweep.json`: public (health/ice/config/backgrounds/docs/openapi),
rooms/tokens/join/info/messages/chat (public и admin), files (upload/download/
bad sig/no auth), recordings gating, STT 501, assistant local-only, все admin
GET (overview, rooms, detail, audit, security, providers, scale, cluster, roles,
access-policy, webhooks, ai-agent, knowledge-agent, in-call-agent, recordings,
files, transcript, analysis, locations), providers/test, scale/turn/test(404),
analysis/run, invites, lock/unlock(423), kick(404), custom roles CRUD (создание/
токен с roleId/update/delete, reserved/дубли/лимит), валидация (битый JSON,
неверные поля, 404/`/mcp`/`/api/*`), «сервер жив после ошибок».

Все ответы соответствовали контракту; 5xx не было; process не падал.

## 4. Найденные проблемы и фиксы

### P1 (исправлено в коде, требует деплоя) — join rate-limit блокирует NAT/офис

- **Симптом:** `live-room --users=60` → только 10 участников, остальные
  `429 rate_limited` («Too many join attempts»).
- **Причина:** `createRateLimiter({max:10})` на (IP, room) в `services.ts`;
  один публичный IP офиса/NAT/нагрузочного генератора.
- **Фикс:**
  - `AGIL_JOIN_RATE_LIMIT_PER_MIN` (default **120**) в `env.ts`/`services.ts`;
  - брутфорс пароля по-прежнему ограничен отдельным lockout
    (`maxFailedJoinAttempts`/`lockoutSec`) — защита не ослаблена;
  - регрессионный тест «office NAT: >10 joins from one IP are allowed within a
    minute» + документация (`README`, `docs/api.md`).
- **Проверено:** локальная сборка — `live-room --users=60` → **60/60**.

### P2 (исправлено) — ожидаемые ошибки логировались как ERROR со стеком

- **Симптом:** каждый 501 STT/assistant, 502 provider, 405 внешнего endpoint
  попадал в `ERROR` с полным stack (`docker logs`), что шумит в мониторинге.
- **Фикс:** в `http.ts` ожидаемые коды (`stt_not_configured`,
  `assistant_local_only`, `provider_*`, `rate_limited`, 4xx) теперь:
  4xx — без лога; ожидаемые 5xx/429 — `WARN` одной строкой без stack; `ERROR`
  только для непредвиденных 5xx.
- **Проверено:** локально `local-server-logging.log` — WARN без stack, ERROR нет.

### P3 (не серверный баг) — конфигурация стенда: внешние AI/knowledge endpoints

На стенде `session.aiAgent.endpoint`/`knowledgeAgent.endpoint` указывают на
`https://example.com` (HTTP 405). Сервер корректно деградирует: локальный
compliance-анализ продолжился, знания — 502, аудит записан. Рекомендация:
обновить конфиг тенанта на реальные endpoint’ы компании (или очистить, чтобы
не было ложных WARN).

### P3 (ожидаемое поведение) — admin rate-limit 120/мин

Интенсивный sweep упирается в `429` на admin API (tenant-scoped fixed window).
Это соответствует дизайну; скрипт теперь делает backoff/retry. Для дашборда
рекомендуется батчинг запросов.

## 5. Регрессия после фиксов (local, свежий build)

```
npm run build -w @agil/calls-server     OK
npm run typecheck -w @agil/calls-server OK
npm test -w @agil/calls-server          76/76 pass
node scripts/security-audit.mjs         32/32 pass
node scripts/e2e.mjs --only=api         11/11 pass
```

## 6. Остаточные риски

- P1-фикс ещё **не задеплоен** на `calls.guardify.ru` (в коде/тестах/доках
  есть; требуется пересборка образа и деплой). До деплоя 60+ участников с
  одного IP получат 429.
- Mesh-режим: сервер не ограничивает media-нагрузку (mesh до ~8–12 участников
  комфортно); 100 WS-участников без медиа сигналинг держит (RAM ~42 MiB), но
  реальные 100 видеопотоков требуют SFU (roadmap, конфиг `scale.sfu`).
- Внешние AI/knowledge endpoint’ы на стенде настроены на заглушку (P3).
- Expired/истёкшие токены и OIDC-сценарии — вне этого прогона (нет доступа к
  секрету/IdP); покрыты unit-тестами `@agil/protocol`/server.
