fix(cli): не отдавать stdin обёртки командам обслуживания
docker compose exec -T подключает stdin и съедал его у скрипта, поданного на stdin (ssh … bash -s): обёртка обрывала такой скрипт после первой команды. Найдено живой проверкой build/cli-live-check.sh (create-user выполнялся, дальше скрипт молча заканчивался). Командам обслуживания stdin не нужен: </dev/null. Плюс e2e web/e2e/query-retry.spec.ts: живая проверка повтора при 429 — запрос участников дважды получает 429 с retry_after_ms и проходит с третьей попытки, раздел «Участники» заполняется без кнопки «Повторить».
This commit is contained in:
@@ -228,10 +228,13 @@ cmd_verify_backup() {
|
||||
|
||||
# app_cli выполняет подкоманду обслуживания внутри контейнера приложения
|
||||
# (AGENT.md 10.7): команды, которым нужна БД, живут в Go-бинарнике, а shell-обёртка
|
||||
# доставляет их с хоста.
|
||||
# доставляет их с хоста. `</dev/null` обязателен: `docker compose exec -T`
|
||||
# подключает stdin и без этого съедает его у вызывающего скрипта — а обёртку
|
||||
# часто зовут из скрипта, поданного на stdin (`ssh … bash -s`), и он обрывался
|
||||
# после первой же команды.
|
||||
app_cli() {
|
||||
require_stack
|
||||
${COMPOSE} exec -T app glchat "$@"
|
||||
${COMPOSE} exec -T app glchat "$@" </dev/null
|
||||
}
|
||||
|
||||
# cmd_reindex пересобирает индекс полнотекстового поиска (AGENT.md 10.7).
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
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/<id>` здесь не
|
||||
// годится: он на мгновение монтирует вкладку «Мои серверы», та заводит
|
||||
// запрос участников по тому же ключу и, размонтируясь, обрывает его
|
||||
// повтор — запрос остаётся в состоянии ошибки.
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user