docs(telegram): invert chat-gate strategy in docs; tune logs; i18n text
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s

- Bake the final default-allow + mute-the-ineligible strategy into
  docs/ARCHITECTURE.md, docs/FUNCTIONAL.md (+_ru), platform/telegram/README.md,
  the deploy compose comment and the PRERELEASE tracker. The live test proved a
  per-user grant cannot exceed a deny-by-default group (Telegram intersects the
  chat default with the per-user permission), so the chat allows sending by
  default and the bot restricts the ineligible instead of granting the eligible.
- Lower the per-event chat_member trace and eligibility evaluation to Debug;
  keep the actual mute/unmute actions, the startup self-check and warnings at
  Info, so prod logs only what the bot did.
- Update game.searchingForOpponent (Searching -> Waiting for opponent / Поиск ->
  Ждём соперника) and the quickmatch e2e assertions to match.
This commit is contained in:
Ilia Denisov
2026-06-21 17:15:10 +02:00
parent bdd1cc7d85
commit 1ba789a1f1
10 changed files with 70 additions and 56 deletions
+8 -8
View File
@@ -19,7 +19,7 @@ test('quick game: enter immediately, wait for an opponent, then it joins', async
// Still waiting for an opponent: the opponent card shows the placeholder, and resign (in the
// history panel) is disabled.
await expect(page.getByText(/Searching for opponent/)).toBeVisible();
await expect(page.getByText(/Waiting for opponent/)).toBeVisible();
await page.locator('.scoreboard').click(); // open the history panel
await expect(page.getByRole('button', { name: 'Drop game' })).toBeDisabled();
@@ -28,7 +28,7 @@ test('quick game: enter immediately, wait for an opponent, then it joins', async
// The opponent card shows its name, the placeholder is gone, and resign is enabled again.
await expect(page.getByText('Robo')).toBeVisible();
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
await expect(page.getByText(/Waiting for opponent/)).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Drop game' })).toBeEnabled();
});
@@ -48,7 +48,7 @@ test('AI game: 🤖 opponent, no wait, chat disabled, dictionary still works', a
// The player lands in an active game at once (no "searching" wait); the opponent shows as 🤖.
await expect(page.locator('[data-cell]').first()).toBeVisible();
await expect(page.locator('.scoreboard').getByText('🤖')).toBeVisible();
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
await expect(page.getByText(/Waiting for opponent/)).toHaveCount(0);
// An AI game has no chat at all: the comms hub drops the Chat tab and lands on the Dictionary
// alone, which stays usable.
@@ -96,7 +96,7 @@ async function enterOpenGame(page: import('@playwright/test').Page): Promise<voi
await page.getByRole('button', { name: 'Random player' }).click();
await page.locator('.variant').first().click();
await page.getByRole('button', { name: /Start game/i }).click();
await expect(page.getByText(/Searching for opponent/)).toBeVisible();
await expect(page.getByText(/Waiting for opponent/)).toBeVisible();
}
test('quick game: a poll recovers a join missed while the live stream is down', async ({ page }) => {
@@ -106,7 +106,7 @@ test('quick game: a poll recovers a join missed while the live stream is down',
await page.evaluate(() => (window as unknown as { __stream: { drop(): void } }).__stream.drop());
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
await expect(page.getByText('Robo')).toBeVisible();
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
await expect(page.getByText(/Waiting for opponent/)).toHaveCount(0);
});
test('quick game: a stream reconnect recovers a join missed while it was down', async ({ page }) => {
@@ -117,7 +117,7 @@ test('quick game: a stream reconnect recovers a join missed while it was down',
await page.evaluate(() => (window as unknown as { __mock: { joinOpponent(): void } }).__mock.joinOpponent());
await page.evaluate(() => (window as unknown as { __stream: { restore(): void } }).__stream.restore());
await expect(page.getByText('Robo')).toBeVisible();
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
await expect(page.getByText(/Waiting for opponent/)).toHaveCount(0);
});
test('quick game: a foreground resync recovers a join shed while the stream stayed alive', async ({ page }) => {
@@ -125,11 +125,11 @@ test('quick game: a foreground resync recovers a join shed while the stream stay
// The opponent joins but the event is shed with the stream still alive (no reconnect, and the
// poll only runs while the stream is down): nothing has recovered yet.
await page.evaluate(() => (window as unknown as { __mock: { joinOpponentSilently(): void } }).__mock.joinOpponentSilently());
await expect(page.getByText(/Searching for opponent/)).toBeVisible();
await expect(page.getByText(/Waiting for opponent/)).toBeVisible();
// Returning to the foreground resyncs the open game (pageshow drives goForeground).
await page.evaluate(() => window.dispatchEvent(new Event('pageshow')));
await expect(page.getByText('Robo')).toBeVisible();
await expect(page.getByText(/Searching for opponent/)).toHaveCount(0);
await expect(page.getByText(/Waiting for opponent/)).toHaveCount(0);
});
// Regression: an opponent joining must not freeze the screen. The in-game live-event effect tracked
+1 -1
View File
@@ -63,7 +63,7 @@ export const en = {
'game.yourTurn': 'Your turn',
'game.yourTurnBy': '{name}: Your turn!',
'game.opponentsTurn': "Opponent's turn",
'game.searchingForOpponent': 'Searching for opponent…',
'game.searchingForOpponent': 'Waiting for opponent…',
'game.waiting': "Waiting for {name}",
'game.makeMove': 'Make move',
'game.reset': 'Reset',
+1 -1
View File
@@ -64,7 +64,7 @@ export const ru: Record<MessageKey, string> = {
'game.yourTurn': 'Ваш ход',
'game.yourTurnBy': '{name}: Ваш ход!',
'game.opponentsTurn': 'Ход соперника',
'game.searchingForOpponent': 'Поиск соперника...',
'game.searchingForOpponent': 'Ждём соперника',
'game.waiting': 'Ожидаем {name}',
'game.makeMove': 'Сделать ход',
'game.reset': 'Сброс',