import { expect, test } from '@playwright/test'; import { apiAs, createGuild, displayName, dropStaleGuilds, ensureProbe, openApp, openGuild, stepUp, type Account, type ProbeLogin, type Session, } from './support'; /** * Лимит частоты API (AGENT.md 8.6): запрос, получивший 429, клиент повторяет * сам, выждав паузу `retry_after_ms` из ответа сервера. До этой правки раздел * «Участники» под нагрузкой оставался пустым с кнопкой «Повторить» — именно * так падал пункт 3 матрицы 11.6 в финальном прогоне 26.09. * * Проверка живая: запрос участников перехватывается в браузере и дважды * получает 429 с настоящим конвертом ошибки, после чего уходит на стенд. * Раздел обязан показать участника без действий пользователя. * * GLCHAT_E2E_REALTIME=1 GLCHAT_URL=https://gl.mhspx.su \ * PLAYWRIGHT_BROWSERS_PATH=../.cache/ms-playwright \ * npx playwright test e2e/query-retry.spec.ts */ const enabled = process.env.GLCHAT_E2E_REALTIME === '1'; const API = '/api/v1'; const PREFIX = 'Retry E2E '; const OWNER_LOGIN: ProbeLogin = { username: 'rtprobe', password: 'Rt-Probe-Owner-9x' }; let owner: Account; test.describe('лимит частоты: клиент повторяет запрос без действий пользователя', () => { test.skip(!enabled, 'нужен развёрнутый инстанс: GLCHAT_E2E_REALTIME=1'); test.beforeAll(async ({ playwright }) => { const ownerApi = await playwright.request.newContext({ baseURL: process.env.GLCHAT_URL }); try { const probe = await ensureProbe(ownerApi, OWNER_LOGIN.username, OWNER_LOGIN.password); const name = await displayName(ownerApi); await stepUp(ownerApi, OWNER_LOGIN.password); await dropStaleGuilds(ownerApi, PREFIX); owner = { ...probe, displayName: name, state: await ownerApi.storageState(), browserState: await ownerApi.storageState(), }; } finally { await ownerApi.dispose(); } }); test('429 на списке участников: раздел заполняется сам', async ({ browser, playwright }) => { const ownerApi = await apiAs(playwright, owner.state); let session: Session | null = null; let guildId = ''; try { const guild = await createGuild(ownerApi, `${PREFIX}${String(Date.now() % 100_000)}`); guildId = guild.id; session = await openApp(browser, owner.browserState); const page = session.page; // Перехват ставим до первого запроса участников: его делает уже список // участников в ленте сервера, и без перехвата данные легли бы в кэш // раньше проверки. Запрос обязан получить 429 дважды и уйти на стенд // только с третьей попытки. let blocked = 0; await page.route('**/api/v1/guilds/*/members*', async (route) => { if (blocked >= 2) { await route.continue(); return; } blocked += 1; await route.fulfill({ status: 429, contentType: 'application/json', body: JSON.stringify({ error: { code: 'rate_limited', message: 'Слишком много запросов. Подождите немного.', retry_after_ms: 400, }, }), }); }); // Открываем настройки сервера так же, как пользователь: выбрать сервер в // рейке и нажать ⚙. Адресный переход `/settings/servers/` здесь не // годится: он на мгновение монтирует вкладку «Мои серверы», та заводит // запрос участников по тому же ключу и, размонтируясь, обрывает его // повтор — запрос остаётся в состоянии ошибки. await openGuild(page, guild.name); await page.getByRole('button', { name: 'Настройки сервера' }).click(); await expect(page.getByTestId('settings-content')).toBeVisible({ timeout: 30_000 }); // Кнопка «Повторить» не нажимается: строку даёт автоматический повтор. const row = page .getByTestId('guild-members') .locator('li', { hasText: `@${owner.username}` }); await expect(row).toContainText(owner.displayName, { timeout: 30_000 }); expect(blocked, 'оба ответа 429 были отданы перехватчиком').toBe(2); } finally { await session?.context.close(); if (guildId !== '') { await stepUp(ownerApi, OWNER_LOGIN.password).catch(() => undefined); await ownerApi.delete(`${API}/guilds/${guildId}`).catch(() => undefined); } await ownerApi.dispose(); } }); });