[report][#120] Автотест offline-sync — подробный отчёт (PWA/офлайн: 6 багов вкл. 3 HIGH data-loss, граница WIP, мета-отчёт) #220
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Автономное тестирование PR #120
feature/offline-syncчерез web-test-orchestrator (30 агентов: recon → 6 персон-слайсов под офлайн → независимый verifier → синтез). Стенд: продакшн-билд клиента, отдаваемый сервером same-origin на :3000 (service worker реально активен), Playwright сset_offlineна одном контексте (SW активируется до офлайна).PR — WIP (M0–M2), поэтому findings разделены на реальные баги vs ещё-не-реализовано (не баг).
🔴 Подтверждённые баги — offline-sync (6)
Суть: ядро PWA-обещания сломано — офлайн-reload белит весь экран; а офлайн-структурные правки (create/move/comment) показывают ложный оптимистичный успех и молча теряют данные при reload-в-офлайне (paused-мутации дегидратируются в IndexedDB, но без
mutationFn/setMutationDefaultsне реплеятся). In-session реплей работает → создаёт ложное впечатление надёжности.[high] Offline reload/relaunch of ANY authenticated route white-screens the entire app (auth gate on an un-persisted currentUser query)
Repro: 1) Persistent chromium context, open http://localhost:3000, log in admin@test.local; wait navigator.serviceWorker.ready and confirm controller!=null. 2) context.set_offline(True). 3) page.reload() on /home (also /s//p/, /settings/...). Observe a solid-white page; contrast: in the SAME offline context /login renders the full form and /p/ renders Error404, so the SW-served shell is healthy — only -gated routes blank.
Evidence: Offline reload of /home: HTTP 200 (not a chrome neterror — #main-frame-error is null), document.title='Home - Gitmost' (React executed from precache) but document.body.innerText.length=0 and #root renders empty; full-page screenshot solid white. Same on /settings/account/profile and /s/general/p/. Persisted RQ blob (IndexedDB keyval-store/gitmost-rq-cache, 12-13 queries) has has_currentUser=False while structural roots (pages/space/spaces/root-sidebar-pages/recent-changes) ARE persisted, and y-indexeddb page. bodies exist — the data is all there but unreachable.
Root cause: apps/client/src/features/user/user-provider.tsx gates the whole Layout subtree on useCurrentUser() (queryKey ['currentUser']); offline POST /api/users/me fails with a no-response network error (API is NetworkOnly in the SW), the 404 branch (error?.['response']?.status===404) is false, so line
if (error) return <></>renders an empty fragment for every route. ['currentUser'] is NOT in OFFLINE_PERSIST_ROOTS (apps/client/src/features/offline/query-persister.ts) and is not warmed by make-offline.ts, so it is never restored. No 'you are offline' fallback is shown.Fix: Persist/warm the ['currentUser'] query for offline (add to OFFLINE_PERSIST_ROOTS + warm in make-offline) and/or fall back to the cached/last-known user (placeholderData) when offline instead of returning <></>; render an offline fallback instead of a blank fragment.
found by: pwa-sw-shell, offline-read (also surfaced by offline-edit-body, update-prompt-install)
[medium] 'Make available offline' pinned pages are unreadable after an offline relaunch (same auth-gate defeats the offline-read promise)
Repro: Online: open/pin a page; the page DATA lands in IndexedDB (keyval-store/gitmost-rq-cache roots pages/space/root-sidebar-pages + y-indexeddb body). context.set_offline(True), then cold-reload /s/general/p/.
Evidence: After offline cold reload of /s/general/p/9ILgHuwVOc: document.body.innerText==='' (entirely white), NOT a redirect to /login (no email/password input), #root has only 2 inert portal children (Notifications/PwaUpdatePrompt), app JS booted from SW cache. The ONLY render-gating failed request was POST /api/users/me (net::ERR_INTERNET_DISCONNECTED). The cached page content sits in IndexedDB but is never shown.
Root cause: Same as the white-screen high finding: make-offline.ts warms page/space/tree/comments/Yjs but never warms ['currentUser']; query-persister OFFLINE_PERSIST_ROOTS omits it, so UserProvider blanks the Layout before the cached page can render. Claims-oracle violation: the action advertises offline readability; the cold-start/relaunch path white-screens. In-session offline WITHOUT reload works (currentUser cache survives in memory) — breakage is reload/cold-start specific.
found by: pwa-sw-shell
[medium] useCollabToken retry predicate throws an uncaught TypeError offline (error.response.status without optional chaining) and corrupts retry/backoff
Repro: Confirm SW controlling, context.set_offline(True), reload an authed page; capture page.on('pageerror').
Evidence: Exactly one uncaught pageerror on every offline boot: "TypeError: Cannot read properties of undefined (reading 'status')", stack 'at retry (.../assets/index-*.js)'. Verified it is distinct from the ERR_INTERNET_DISCONNECTED console noise; also caught as an unhandledrejection.
Root cause: apps/client/src/features/auth/queries/auth-query.tsx:27 —
if (isAxiosError(error) && error.response.status === 404). Offline the collab-token POST fails as an axios NETWORK error: isAxiosError===true but error.response===undefined, so .status throws. (React Query's onlineManager hard-inits _online=true and never reads navigator.onLine, so with networkMode default 'online' the query still runs offline and hits this path.) The throw happens in retryer .catch before reject(), so the query's thenable never settles and retry/backoff is corrupted. Sibling handlers (user-provider.tsx, login-form.tsx, api-client.ts:24if (error.response)) all guard; this one does not.Fix: Change to
error.response?.status === 404.found by: offline-read, pwa-sw-shell, update-prompt-install (3 independent agents)
[high] Offline structural CREATE is a paused in-memory mutation: replays in-session but is SILENTLY LOST on reload/close while offline (no error, no replay)
Repro: Open page online (SW controller!=null). set_offline(True). Click sidebar Create (+). RELOAD while still offline. set_offline(False), wait ~16s. Compare to the same flow WITHOUT the offline reload.
Evidence: Scenario A (no reload): on reconnect POST /api/pages/create -> 200 (replayed). Scenario B (reload while offline): on reconnect ZERO pages/create requests, no toast, no console error -> silent loss. The paused mutation IS dehydrated into IndexedDB (keyval-store/gitmost-rq-cache: 'mutations=1') but cannot be replayed.
Root cause: useCreatePageMutation (page-query.ts) sets no mutationKey; QueryClient (main.tsx) sets no networkMode and there is NO setMutationDefaults/MutationCache anywhere, so after a reload resumePausedMutations finds a restored paused mutation with no mutationFn and no-ops. The in-session replay creates a false durability impression; a reload/tab-close drops the write with zero user signal.
found by: offline-structural-outbox
[high] Offline MOVE shows optimistic success (tree reorders) with no pending indicator; replays in-session but is silently lost on reload-while-offline
Repro: Open /s/general online, wait for SW. set_offline(True). Drag a sidebar row above a sibling. Observe order. set_offline(False) wait ~14s (and separately the reload-while-offline path).
Evidence: Offline drag reordered the sidebar optimistically with no error and no 'offline/pending' affordance (a body /offline|pending/ scan matched only a page title — confirmed FALSE pending indicator). No /api/pages/move fires offline. In-session reconnect: POST /api/pages/move -> 200 (replay). Reload-while-offline then reconnect: zero move requests and the final server order == original pre-move order = move silently lost.
Root cause: use-tree-mutation.ts handleMove does setData(optimistic) into the non-persisted jotai treeDataAtom BEFORE the paused movePageMutation.mutateAsync; useMovePageMutation has only mutationFn (no mutationKey), no setMutationDefaults, default networkMode 'online' -> dehydrated paused mutation has no rehydratable mutationFn, and the optimistic tree state is not persisted. Net: false success + silent data loss on reload with no warning.
found by: offline-structural-outbox
[medium] Offline COMMENT: Send spins forever and the input is cleared immediately (looks submitted) while nothing posts; replays in-session, lost on reload
Repro: Open page online, open Comments panel, set_offline(True), type a comment, click Send (arrow). Observe button + list. set_offline(False) wait ~16s; check server.
Evidence: Offline: editor inner_text becomes '\n' immediately on click (typed text gone — handleSave clears content synchronously), the Send ActionIcon shows data-loading='true' indefinitely (isPageCommentLoading reset only in mutateAsync's finally, which never runs offline), zero /api/comments requests, panel stays '0 Open'. In-session reconnect: one POST /api/comments/create -> 200, marker appears, '1 Open'. Reload-while-offline: the paused mutation IS in gitmost-rq-cache but after reload+reconnect zero create requests and the marker is gone forever.
Root cause: useCreateCommentMutation default networkMode 'online', no mutationKey/outbox, no resumePausedMutations wiring (same mechanism as CREATE/MOVE). Data-loss-after-apparent-success: input cleared + permanent spinner imply submission while the comment is dropped on reload.
found by: offline-structural-outbox
🟡 Подтверждённые баги — gitmost-ui / pre-existing (2)
[medium] Offline CREATE gives zero feedback (silent no-op), then on reconnect abruptly navigates to the new page seconds later
Repro: Open page online, set_offline(True), click sidebar Create (+). Observe (no node/nav/toast/spinner/error). set_offline(False), wait and watch the route.
Evidence: Offline after the click: URL unchanged, sidebar /p/ links 41->41 (no new node), toasts [], spinners 0, console errors [], ZERO /api/pages requests (POST is paused, never leaves the browser). On reconnect POST /api/pages/create -> 200 fires (~5.8s here, ~16s with a longer offline window — proportional to offline duration since it's a resuming paused mutation) and the route abruptly changes to a brand-new untitled page, sidebar 41->42, with no prior pending indication.
Root cause: use-tree-mutation.ts handleCreate awaits createPageMutation.mutateAsync (paused offline, no onError) BEFORE the optimistic node insert/navigate. A user offline sees nothing happen and will click again, queueing DUPLICATE 'Untitled' pages on reconnect. Even before a full outbox, the action must signal offline/queued/error rather than silently no-op then surprise-navigate.
found by: offline-structural-outbox
[medium] collab-token throttled to 5 per minute breaks live collab on the 6th page open within a minute (pre-existing upstream, NOT a PWA regression)
Repro: Open 7 pages within ~17s, or POST /api/auth/collab-token 6 times in a minute.
Evidence: 6 sequential collab-token POSTs returned 200 200 200 200 200 429; a 7-page repro gave 5x200 then 429 429 429. The 6th+ page's editor never gets a token, so it cannot sync/edit (and contributes to the read-only-until-synced body state).
Root cause: collab-token only skips the AUTH and AI_CHAT throttlers while PUBLIC_SHARE_AI_THROTTLER (limit 5/min) still applies. Pre-existing in the upstream/base, surfaced as test friction (429 storms) — flagged for awareness, not introduced by this PR.
found by: online-sanity-regression
⚪ Ещё не реализовано (WIP, НЕ баги — граница офлайн-покрытия) (5)
✅ False positives (убиты verifier-проходом) (5)
Подробный мета-отчёт
QA Synthesis — PR #120
feature/offline-sync(Gitmost / Docmost fork, OFFLINE + PWA, WIP M0–M2)1. Stand & method
prompt, API/collab/socket.io = NetworkOnly).navigator.serviceWorker.ready+controller != null→context.set_offline(True)→ reload/observe →set_offline(False)→ verify sync. SW + IndexedDB state persisted across transitions. Evidence-before-claim enforced; every defect has a captured artifact (HTTP status / pageerror / screenshot / IndexedDB read / clean-context server check).2. Agents that ran & how each stage went
reconpwa-sw-shelloffline-readoffline-edit-bodyoffline-structural-outboxupdate-prompt-installonline-sanity-regression3. Real-vs-false tally
4. Who-found-what (unique credit)
pwa-sw-shell+offline-read(independent).pwa-sw-shell.offline-read,pwa-sw-shell,update-prompt-install(triple-found).offline-structural-outbox.online-sanity-regression.5. Offline coverage boundary (what actually works end-to-end vs not)
WORKS (verified):
promptsemantics with no false nags.BROKEN (defects — the caching exists but is defeated):
currentUserquery) — defeats ALL cached/pinned offline reading. This is the dominant issue: a ~3-line fix unlocks the whole offline-read feature.SILENT DATA LOSS (defects — false durability):
NOT BUILT (WIP, not bugs): durable outbox/replay (M2), offline cold-read of non-opened/non-pinned pages, write-time offline/pending UX, and an offline fallback screen. See
notImplementedWip.6. Concrete improvement suggestions (ranked)
currentUserquery — add'currentUser'to OFFLINE_PERSIST_ROOTS + warm it in make-offline, and/or give UserProvider a cachedplaceholderData/last-known-user path soif (error) return <></>doesn't blank offline. Highest leverage: fixes both the HIGH white-screen and the pinned-page MED in one shot.auth-query.tsx:27→error.response?.status === 404— one-line fix removes the uncaught TypeError + retry/backoff corruption on every offline boot.mutationKeys +setMutationDefaults(with default mutationFns) soresumePausedMutationscan replay dehydrated paused mutations after reload; considernetworkMode:'offlineFirst'. This converts the three silent-data-loss defects into durable queued writes.collab-tokenfrom PUBLIC_SHARE_AI_THROTTLER (or raise/scope the limit) so opening >5 pages/min doesn't break live collab — also reduces 429 test friction.7. Honest coverage gaps / caveats
set_offline; this confounded the title offline→reconnect-sync test (left speculative) and contributed to one anomalous duplicated-title server write that is NOT asserted as a deterministic defect.🤖 web-test-orchestrator (multi-agent QA), прогон по PR #120