feat(offline): implicit net-state model, two-tier version gate, unified lobby
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m12s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s
Land the offline-model redesign (ANDROID_PLAN.md O1-O7): replace the explicit offline toggle with a single detected net-state machine, unify the lobby, and add a two-tier client-version gate. Contour-safe: both version vars empty => dormant, the wire change is additive, so web/pwa/vk/tg behaviour is unchanged unless the gate is deliberately configured. O1 net-state reducer (test-first). O2 store + wiring (connection/offline shims; +@capacitor/network). O3 remove the offline toggle + migrate the pref. O4 two-tier gate: hard update_required degrades to an offline Update/Play-offline notice (not terminal); soft GATEWAY_RECOMMENDED_CLIENT_VERSION -> X-Update-Recommended nudge. O5 unified lobby (device-local + greyed-from-cache server games; self-set identity; closes G-step-0). O6 create flows (with-friends online/offline segment + offline dict guard). O7 docs. Telegram/VK stay online-only (contour review): the offline model is channel-gated via offlineCapable() (native + plain web; false in the mini-apps). offlineMode.active is hard-gated on it, so the whole model (blue chrome, unified/greyed lobby, transport kill switch, device-local vs_ai/hotseat create) stays inert in Telegram/VK regardless of the detected net state (closing a version-lock path that could have flipped them offline); and New Game's with-friends hides the online/offline segment there, leaving the remote invite alone. ARCHITECTURE already declared "Telegram/VK are exempt" - this makes the code match. Deploy/CI: wire the version gate through the deploy (GATEWAY_MIN_CLIENT_VERSION + GATEWAY_RECOMMENDED_CLIENT_VERSION as plain unprefixed vars via compose + ci.yaml + prod-deploy.yaml + write-prod-env.sh + .env.example) so the gate is live when set (test-contour commit-hash version fails open; empty => dormant). Disable the manual android-build CI workflow for now (rename .disabled). Bump the app bundle budget 127->130 KB for the added always-loaded wiring. Fix the UI Docker stage (gateway/Dockerfile): install --ignore-scripts so the Alpine/musl SPA build no longer tries to native-build sharp (a local android:assets tool, unused in the image); esbuild's binary is an optional dep so Vite still builds. Tests: gateway go green (two-tier gate over a real HTTP handler); docker build of the landing + gateway targets green locally; compose --no-interpolate confirms the gateway env; two new telegram.spec.ts e2e (offline signal keeps the chrome online + quick-match opponent choice visible => server enqueue; with-friends shows no pass-and-play), RED-verified; svelte-check 0/0, vitest 617, e2e 248 (chromium + webkit), build + bundle-size 127.8/130.
This commit is contained in:
@@ -122,6 +122,54 @@ test('outside Telegram, the /telegram/ entry shows the launch diagnostic, not a
|
||||
await expect(page.getByRole('button', { name: /guest/i })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('inside Telegram (online-only), an offline signal never switches the app to offline mode', async ({
|
||||
page,
|
||||
}) => {
|
||||
// The Telegram mini-app is online-only: the offline model (implicit offline, device-local games,
|
||||
// the transport kill switch) must stay inert there. Even when the net-state machine is driven to an
|
||||
// offline state, offlineMode must remain false — otherwise the chrome turns blue, the lobby greys the
|
||||
// server games and, critically, a vs_ai start would create a device-local game instead of enqueuing.
|
||||
await page.addInitScript((stub) => {
|
||||
Object.assign(window, stub);
|
||||
}, webAppStub());
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Your turn')).toBeVisible(); // the durable mini-app session, in the lobby
|
||||
|
||||
// Drive the net-state machine to an offline state through the mock hook.
|
||||
await page.evaluate(() => (window as unknown as { __net: { offline(): void } }).__net.offline());
|
||||
|
||||
// Because the channel is online-only, offlineMode stays inert: on the lobby the chrome never turns blue
|
||||
// and no server game is greyed…
|
||||
await expect(page.locator('header.nav.offline')).toHaveCount(0);
|
||||
await expect(page.locator('.rowwrap.greyed')).toHaveCount(0);
|
||||
// …and New Game's quick match still offers the opponent choice (AI / random), which is hidden only in
|
||||
// real offline mode. Its presence is the positive proof that offlineMode is false — so the vs_ai start,
|
||||
// whose device-local branch is gated on offlineMode, enqueues on the server instead of creating a
|
||||
// device-local game.
|
||||
await page.locator('button.tab').nth(0).click();
|
||||
await expect(page.locator('button.opt', { hasText: '🤖' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('inside Telegram (online-only), New Game with friends offers remote invites only', async ({
|
||||
page,
|
||||
}) => {
|
||||
// A Telegram launch authenticates a durable account, so New Game shows the auto/with-friends selector.
|
||||
// "Play with friends" must offer only the remote invite — never the online/offline segment, whose
|
||||
// "Pass and play" is the device-local hotseat flow that has no place in an online-only mini-app.
|
||||
await page.addInitScript((stub) => {
|
||||
Object.assign(window, stub);
|
||||
}, webAppStub());
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Your turn')).toBeVisible();
|
||||
|
||||
await page.locator('button.tab').nth(0).click(); // the New tab opens the create screen
|
||||
await page.getByRole('button', { name: 'Play with friends' }).click();
|
||||
|
||||
// The online/offline segment (Invite a friend / Pass and play) is absent — only the invite form shows.
|
||||
await expect(page.getByRole('button', { name: 'Pass and play' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Invite a friend' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('a blocked telegram-web-app.js does not hang the diagnostic screen', async ({ page }) => {
|
||||
// Simulate a network where telegram.org is unreachable: the SDK fetch fails. Because the SPA
|
||||
// loads the SDK dynamically with a timeout (not a render-blocking <script>), a failed/blocked
|
||||
|
||||
Reference in New Issue
Block a user