Commit Graph

24 Commits

Author SHA1 Message Date
grendervill 7512857363 test(e2e): матрица 11.6 для иконки беседы и личных шаблонов (Фаза 7)
Две проверки на стенде двумя браузерами, без F5 (trackLoads = 0):

* группа из трёх пробных аккаунтов: владелец ставит иконку и переименовывает
  беседу через меню в шапке, наблюдатель-участник видит иконку в шапке и в
  списке бесед, новое имя — там же, событием DM_CHANNEL_UPDATE; участнику
  меню владельца недоступно;
* личные шаблоны: первое устройство сохраняет структуру сервера шаблоном,
  второе (свой вход формой) открывает окно создания сервера, видит шаблон в
  «Моих шаблонах» и создаёт по нему сервер — комната и роль из шаблона на
  месте. У личных шаблонов нет события шлюза: список читается при открытии
  окна, перезагрузка страницы не нужна.

Имя беседы сверяется ещё и по REST, тестовые данные убираются (группа
распускается вместе с иконкой, серверы и шаблон удаляются, данные прошлых
прогонов подчищаются в beforeAll).
2026-09-26 17:38:28 +03:00
grendervill 12dc41ee7c fix(e2e): точный локатор кнопки входа в общей форме
Кнопка «Войти по ключу доступа» тоже подходит под /Войти/u, поэтому
Playwright в strict mode находил два элемента и падал на входе — от этого
зависели все спеки, которые логинятся через форму (voice, realtime-social и
другие), когда панель passkeys успевала отрисоваться.
2026-09-26 17:20:33 +03:00
grendervill 78e9c3c2d6 test(e2e): звонок в личной беседе двумя браузерами (Фаза 7)
Спека поднимает звонок между двумя реальными клиентами на стенде и
проверяет то, что нельзя проверить юнитами: приглашение приходит событием,
оба браузера обмениваются аудио через LiveKit (входящие байты > 0),
выключенный микрофон виден на сервере, завершение снимает маркер у второго
участника, а в истории остаётся запись.

Сессия берётся из api-контекста (cookie __Host-session): форма входа
упирается в лимит попыток при серии прогонов. Запуск по флагу
GLCHAT_E2E_VOICE=1, как у голосовой спеки.
2026-09-26 17:20:33 +03:00
grendervill cca64451e2 test(web): e2e passkeys проверяет отказ после отзыва ключа
После отзыва ключа виртуальный аутентификатор ещё хранит credential и
подписывает challenge, поэтому отказ приходит от сервера
(auth.passkey_unknown): проверяем, что страница входа показывает ошибку и
не пускает в приложение, а не конкретный testid клиентской ошибки.

Живой прогон на стенде: регистрация ключа, вход по нему без пароля и
отзыв — 1 passed (виртуальный аутентификатор Chromium через CDP).
2026-09-26 15:30:14 +03:00
grendervill cd1d662572 feat(auth): вход по ключу доступа (passkeys, WebAuthn)
Фаза 7, AGENT.md 7.1: регистрация ключа в настройках безопасности с
обязательным step-up, вход по ключу без пароля (в т.ч. без ввода почты —
обнаруживаемый ключ), несколько ключей на аккаунт, отзыв и переименование,
события безопасности и аудит.

Сервер: github.com/go-webauthn/webauthn (BSD-3-Clause), RP ID и Origin
берутся из конфига домена; если домен — IP-адрес (стенд без домена),
passkeys честно выключены (auth.passkey_unsupported). Церемонии живут в
памяти процесса 5 минут и одноразовые: повторная отправка challenge
отклоняется. Миграция 00018 пересобирает неиспользуемую таблицу
webauthn_credentials под полную запись credential в JSON. Новые ручки
входа ограничены по IP (10/мин).

Клиент: тонкая обёртка над navigator.credentials без тяжёлых SDK,
раздел «Ключи доступа» в настройках безопасности и кнопка «Войти по ключу»
на экране входа; понятные сообщения для браузеров без поддержки WebAuthn
и при отмене диалога.

Тесты: Go — регистрация/вход с эмулятором аутентификатора (реальная
проверка подписи P-256), отказ при чужом challenge, одноразовость
церемонии, обязательный step-up при управлении ключами, запрет входа
забаненному, ограничение allowCredentials при входе с почтой, лимит и
валидация имени; web — 12 vitest с моком navigator.credentials;
Playwright — живой сценарий с виртуальным аутентификатором Chromium.
2026-09-26 15:12:43 +03:00
grendervill ad7b3d21bb test(web): проверка саундборда дожидается локального воспроизведения
Клиент играет звук асинхронно: сначала скачивает файл через /files/{id} в
blob, и только потом создаёт Audio и зовёт play(). Подпись о проигрывании
появляется раньше звука, а проверка читала счётчики сразу после неё и ловила
гонку: в прогоне build/e2e-all-run8.log пункт 27 падал с __soundCalls = 0,
хотя сервер отвечал 200 и запрос файла уже шёл. Ждём факт запуска через
expect.poll — проверка снова проверяет воспроизведение, а не скорость сети.

Пункт был недоступен раньше: без медиапути LiveKit тест скипался, поэтому
гонка не проявлялась.
2026-09-26 14:42:47 +03:00
grendervill ee334a77bc test(web): голосовые e2e скипаются при недоступном медиапути
Пункты 3, 10 и 11 матрицы и тест саундборда падали не на продукте, а на
инфраструктуре: ICE через TURN на VPS не поднимается (STUN отвечает, но все
ICE-пары `failed` с `requestsReceived: 0`), и проверка трижды ждала кнопку
входа, после чего падал весь прогон.

В support.ts добавлен общий помощник joinVoiceIfPossible/joinVoiceOrSkip:
- три попытки, как раньше, но при уже показанной клиентом ошибке повтор не
  делается, а ожидание ограничено 25 секундами;
- при неудаче проверяется серверная часть входа (POST .../voice/join →
  токен LiveKit): живой сервер при мёртвом ICE — блокер среды, проверка
  скипается с точной причиной; нерабочая ручка — дефект продукта, тест
  падает. Проба ограничена по времени, а ответ 429 считается признаком
  живого сервера (лимит частоты к медиапути отношения не имеет);
- п. 11 при недоступном голосе не отменяется: голосовой мьют пропускается
  с аннотацией и предупреждением в лог, а тайм-аут, кик, возврат сервера в
  рейку и бан по-прежнему проверяются.

Три локальные копии joinVoice заменены одним помощником.

Прогоны на стенде (образ с HEAD): матрица 18 passed / 0 failed / 3 skipped,
§11.5 — 8 passed, два прогона подряд совпали; без правки было 3 failed.
2026-09-26 14:15:53 +03:00
grendervill 73dd51b121 fix(server): правки сервера, ника и комнат попадают в аудит
§11.5 требует, чтобы каждое мутирующее действие писалось в журнал сервера
с `actor.instance_admin=true`, но у PATCH /guilds/{id},
PATCH /guilds/{id}/members/{user_id} и у ручек комнат вызова `recordAudit`
не было: переименование сервера, смена ника, тайм-аут и создание, правка и
удаление комнаты не оставляли следа ни у владельца, ни у инстанс-админа.

- guild.update, member.update, member.timeout (в причине — срок тайм-аута),
  channel.create, channel.update, channel.delete;
- Go-тест TestAuditCoversGuildMemberAndChannelChanges проверяет записи на
  чужом сервере и флаг actor_instance_admin, а также что действие владельца
  этим флагом не помечается;
- e2e 19b в instance-admin.spec.ts включён вместо test.fixme с диагностикой.

Проверки: go test -tags sqlite_fts5 -race -count=1 ./internal/... — ok;
make go-lint — 0 issues; живьём на стенде тест 19b проходит.
2026-09-26 14:15:49 +03:00
grendervill 5a419112e0 test(web): e2e шапки сервера, прав кнопок и саундборда
Проверки, которые в матрице 11.6 обходились по REST, теперь идут через
интерфейс (AGENT.md 7.13, 7.15, 11.6).

- 25: 🔗/⚙/+ видны владельцу и не видны обычному участнику, а после выдачи
  роли с правами управления появляются без перезагрузки;
- 26: статус комнаты и медленный режим появляются в шапке по событию и
  пропадают при снятии;
- 27: саундборд — панель открывается в голосовой комнате, проигрывание
  уходит на сервер (200), звук играет локально (счётчик вызовов
  `Audio.play`, подпись «включил звук»). Если медиапуть LiveKit не
  поднялся, тест скипается с причиной: это внешняя зависимость;
- 28: счётчик непрочитанных в сайдбаре и явная фиксация того, что центра
  уведомлений в клиенте нет (заглушка — только переключатель системных
  уведомлений в настройках).
2026-09-26 11:06:51 +03:00
grendervill 8c2bb3e18c test(web): e2e личных серверов, DM, друзей и блокировок
Сценариев личных серверов и личных бесед в матрице 11.6 не было
(AGENT.md 7.2, 7.8, 7.9). Новый спек идёт под тем же флагом
`GLCHAT_E2E_REALTIME=1` и тем же стилем: два браузера, `support.ts`,
`trackLoads`, уборка в `finally`.

- 21: личный (непубличный) сервер — вход по ссылке-приглашению в браузере,
  сервера нет в каталоге, вход без приглашения 403, вошедший видит историю;
- 22: DM 1:1 — непрочитанное в списке бесед, история, «печатает», ответ из
  интерфейса, правка и удаление без перезагрузки;
- 23: заявка в друзья и её принятие кнопкой в интерфейсе;
- 24: блокировка из списка друзей — писать нельзя в обе стороны
  (`dm.blocked`), беседа не открывается, разблокировка возвращает переписку.

Приглашение (1 на прогон) создаёт владелец: суточная квота — 10 на
пользователя (AGENT.md 8.6).
2026-09-26 11:06:46 +03:00
grendervill 5176b277a6 test(web): e2e инстанс-админа на чужом сервере (§11.5)
Чек-лист §11.5 проверялся отдельными скриптами и живьём, но в спеках его
не было. Новый файл (`GLCHAT_E2E_INSTANCE=1`) работает на сервере, где
админ не участник: владелец — `rtprobe`, участник — `rtprobe2`.

- 14: видимость — чужой приватный сервер в `GET /instance/guilds` и в
  списке серверов админа, комнаты, история, поиск, участники, роли, баны,
  инвайты, эмодзи, звуки, вебхуки, аудит; чужие DM по-прежнему 404;
- 15: управление без 403 — имя сервера, ник владельца, роли (создание,
  поднятие выше своей, выдача и снятие), комнаты; аудит помечает действия
  `actor_instance_admin=true`;
- 16: модерация — бан с причиной и автором, разбан, кик, тайм-аут; попытки
  владельца кикнуть/забанить/замутить/лишить роли админа дают 403
  `instance.admin_protected`;
- 17: обход лимитов — slowmode, приватность комнаты, лимит участников
  голосовой комнаты;
- 18: UI — админ входит в чужой приватный сервер из рейки без инвайта,
  пишет в приватную комнату, а участник видит переименование и кик без
  перезагрузки;
- 19: аудит — прежние записи не затираются; 19b (`test.fixme`) фиксирует
  пробел: переименование сервера, ник и комнаты в аудит не пишутся;
- 20: безопасность не обходится — step-up на свежей сессии, чужой Origin,
  лимит реакций 429.

Уборка тестовых серверов — в `finally` от имени админа (удаление чужого
сервера требует step-up).
2026-09-26 11:06:42 +03:00
grendervill 71c0107b87 test(web): общие помощники живых e2e в support.ts
Новые спеки (§11.5, личные серверы и шапка сервера) используют тот же
набор шагов, что матрица 11.6, но её помощники лежали внутри
`realtime.spec.ts`. Вынес в `support.ts`, чтобы не копировать их в третий
раз: сессии и лимит входов, повтор при 429, step-up (в том числе с 2FA
админа), подготовка сервера и уборка, комнаты, роли, сообщения, звуки,
личные беседы, друзья и блокировки.
2026-09-26 11:06:36 +03:00
grendervill dcafb10046 test(e2e): матрица 11.6 — пункт 12 без fixme и возврат сервера в пункте 11
Пункт 12 включён: клиент уходит на экран входа при отзыве сессий, в том числе
когда открыт сервер (дефект исправлен в 751ecc6), а смена пароля на сервере
больше не рвёт соединение текущего устройства (05409d0).

Пункт 11 теперь проверяет то, ради чего он написан: после кика и повторного
входа плитка сервера возвращается в рейку без перезагрузки (правка клиента в
ea1b4a7). Возврат пароля пробного участника перенесён в finally: падение теста
не оставляет постоянный аккаунт с временным паролем, а неудачный возврат
сообщается явной проверкой.

Прогон: 13 passed (2,9 мин), лог — build/e2e-realtime-run4.log.
2026-09-22 22:28:10 +03:00
grendervill 594a5c0af5 test(e2e): пункт 6 матрицы 11.6 не зависит от суточной квоты приглашений
Приглашения ограничены 10 на пользователя за 24 ч (AGENT.md 8.6), поэтому
проверка инвайтов создаёт отдельного свежего владельца сервера: у постоянных
пробных аккаунтов квота выгорает за серию прогонов. Прогон: 1 passed.
2026-09-22 21:24:22 +03:00
grendervill ab855fbd4f test(e2e): матрица realtime §11.6 — пункты 1–13
Два спектакля на общем модуле `web/e2e/support.ts` (запуск только с
`GLCHAT_E2E_REALTIME=1`):
- `realtime.spec.ts` — пункты 1–7: роли и права, приватная комната по роли,
  профиль и ники, оформление сервера, эмодзи/звуки/косметика, инвайты, комнаты;
- `realtime-social.spec.ts` — пункты 8–13: сообщения, непрочитанные и
  упоминания между устройствами, presence/typing/голос, модерация, два
  устройства, RESUME после реконнекта.

Владелец действует по REST, второй браузер видит результат через Gateway;
полные загрузки документа запрещены и проверяются `trackLoads`. Матрица нашла
четыре дефекта продукта (исправлены в `d241714` и `7ec9dbf`).
2026-09-22 21:19:37 +03:00
grendervill 7ec9dbfdee fix(realtime): уход на вход после отзыва сессии и бан в открытом сервере
- invalidateCurrentUser перечитывает профиль, а не только удаляет кэш: без
  запроса AuthGuard не видит 401 и второе устройство остаётся в приложении
  после logout-all или смены пароля (AGENT.md 11.6, пункт 12);
- сброс сессии и уход из последнего сервера инвалидируют список серверов:
  пустой снапшот подставлял устаревший REST-ответ, и сервер возвращался в
  рейку (видно и после кика);
- joinGuild проверяет бан сервера: публичный сервер больше не обходится
  нажатием «Войти» вместо приглашения (AGENT.md 7.17, 7.20), тест расширен;
- e2e/voice-ten приведён к линту, console.log разрешён в e2e (диагностика
  прогона), артефакты Playwright исключены из prettier.
2026-09-22 20:35:13 +03:00
grendervill 66dd07eb7b feat(ops): генератор нагрузки и подготовка аккаунтов для теста
- cmd/loadgen: держит заданное число WS-клиентов Gateway, шлёт сообщения с
  нужной частотой, измеряет задержку доставки (p50/p95/p99/max), ответы API,
  разрывы соединений и пишет отчёт в JSON;
- glchat create-user (CLI + обёртка): создаёт аккаунты в обход выключенной
  регистрации, пакетно (--count/--prefix), при --sessions сразу выдаёт сессии
  и складывает учётные данные в файл 600 — иначе сотня клиентов не сможет
  войти из-за лимита попыток по IP;
- auth: CreateUserByOperator и IssueSessionForOperator с тестом;
- web/e2e/voice-ten.spec.ts: десять участников в одной голосовой комнате,
  проверка плиток, входящего аудио и слоя 1080p60 у клиента; сессии берутся
  из файла (без формы входа);
- .gitignore: каталоги вывода Playwright test-results*.
2026-09-22 20:00:33 +03:00
grendervill 785af0dc7b feat(guilds): приватные комнаты — права доступа поверх серверных
Сервер:
- store: оверрайды всех комнат сервера одним запросом (без N+1) и снятие
  оверрайда с признаком «был ли он»;
- API: PUT/DELETE /guilds/{id}/channels/{cid}/overwrites/{role|user}/{tid}
  (права именами через `|`, как у ролей), step-up на изменение, проверка что
  роль принадлежит серверу, а участник состоит в нём;
- список комнат отдаёт permission_overwrites; после правки сбрасывается кэш
  прав комнаты, пишется аудит (channel.overwrite_set/delete) и уходит событие
  GUILD_CHANNELS_SYNC — видимость комнаты меняется у всех участников.

Клиент:
- редактор «Доступ к комнате»: приватность одним переключателем (запрет
  VIEW_CHANNEL для @everyone), права просмотра/переписки/входа для ролей и
  участников, подтверждение личности по требованию сервера;
- в настройках комнаты теперь и голосовые комнаты (фон и права), вебхуки —
  только у текстовых; событие GUILD_CHANNELS_SYNC перечитывает список комнат.

Тесты: 2 Go-теста (скрытие и открытие комнаты ролями, проверка цели и прав) и
3 web-теста редактора (маски, step-up, скрытие без MANAGE_ROLES).
2026-09-22 19:14:05 +03:00
grendervill cc16c18b13 fix(web): пинг участника показывается только при наведении
Постоянная подпись в плитке занимала место и была не нужна: задержка теперь
живёт в подсказке индикатора связи («Отличное соединение, 46 мс — задержка до
голосового сервера»), а плитка осталась компактной. Тесты и e2e проверяют
`title`/`aria-label` индикатора вместо видимого текста.
2026-09-20 22:30:24 +03:00
grendervill 96acc40031 test(web): e2e проверяет пинг участника в плитке
Два реальных браузера в одной комнате: у каждого в плитке появляется своя
задержка («N мс», измеряется по WebRTC-статистике), а второй участник видит
пинг первого, пришедший по data-каналу LiveKit.
2026-09-20 22:15:39 +03:00
grendervill 8ae8ca1a1d test(web): e2e голоса различает отсутствие пользователя и лимит частоты
Регистрация тестового пользователя выполнялась на любой неудачный вход, из-за
чего при `rate_limited` прогон падал на попытке регистрации. Теперь:
- 401 → пользователя нет, регистрируем; 429 → ждём `retry_after_ms` и повторяем;
- 409 при регистрации (аккаунт уже есть) → просто входим;
- число повторов увеличено до шести.
Прогон зелёный, аккаунт `voiceprobe` переиспользуется.
2026-09-20 20:57:54 +03:00
grendervill 4a0b70ab24 test(web): e2e голоса переиспользует тестового пользователя и терпит лимиты
- постоянный аккаунт `voiceprobe` вместо регистрации нового на каждый прогон
  (API удаления пользователей пока нет — это Фаза 5);
- `postWithRetry` и повтор входа в браузере учитывают `rate_limited` и
  `retry_after_ms`: защита от флуда входа обязана работать, а тест не должен
  падать из-за неё;
- проверено два прогона подряд: оба зелёные, в базе стенда остаются только
  аккаунт администратора, тестовый пользователь и серверы владельца.
2026-09-20 20:07:10 +03:00
grendervill a6d23a295e fix(voice): административный токен RoomService привязан к комнате
LiveKit принимает право `roomAdmin` только вместе с именем комнаты: без
`video.room` RoomService отвечает 401 `permissions denied` — серверный мьют и
отключение участника не доходили до SFU, хотя API отвечал 200 (AGENT.md 7.14).

- `voice.IssueAdmin(room)` кладёт в грахт `room` и `roomAdmin` и отклоняет
  пустое имя комнаты;
- `AdminClient.call` выпускает токен под конкретную комнату каждого вызова
  (GetParticipant, MutePublishedTrack, RemoveParticipant, ListParticipants);
- тест на состав грахта и отказ при пустой комнате;
- e2e: администратор и гость работают в отдельных api-контекстах (код 2FA
  одноразовый), нажатие «Войти» в комнату не ждёт стабильности исчезающей
  кнопки.
2026-09-20 19:54:21 +03:00
grendervill 912a067c9a test(web): e2e-стенд Playwright для голосовой комнаты
Проверка голоса двумя реальными браузерами (AGENT.md 11.2): два контекста с
фейковыми медиа-устройствами входят в одну комнату, инструментируется
`RTCPeerConnection` и по `inbound-rtp` подтверждается, что аудио реально
доходит; затем серверный мьют блокирует микрофон у гостя, а отключение
участника убирает его плитку у обоих клиентов.

- `playwright.config.ts`: прогон против развёрнутого инстанса (`GLCHAT_URL`),
  трейс при падении, флаги фейковых медиа-устройств;
- `e2e/voice.spec.ts`: подготовка через REST (сервер, комната, инвайт, гость),
  завершение онбординга, уборка тестовых серверов и включение только по
  `GLCHAT_E2E_VOICE=1`;
- `tsconfig.e2e.json` и запись в eslint для новых файлов.
2026-09-20 19:30:46 +03:00