[report][#120] Автотест offline-sync — подробный отчёт (PWA/офлайн: 6 багов вкл. 3 HIGH data-loss, граница WIP, мета-отчёт) #220

Closed
opened 2026-06-26 19:05:28 +03:00 by Ghost · 0 comments

Автономное тестирование 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:24 if (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)

  • Offline cold-read of pages NOT opened/pinned this session (arbitrary Contour-B structural offline): reloading or navigating to a non-cached page while offline yields a blank shell with no content — the structural offline shell (M1/M2) is only partially built. Not a bug per se (this was never the headline Contour-A guarantee), but it is the coverage boundary. Note the failure mode oscillates between blank-white-shell and a redirect to the cached /login form.
  • Durable offline write OUTBOX / mutation-queue (M2): there is no setMutationDefaults, no MutationCache wiring, no mutationKeys, no onlineManager/networkMode config. Offline structural mutations (create/move/comment/etc.) only survive as in-memory paused React Query mutations for the life of the tab; nothing replays them after a reload/close. The persistence half (read queries via query-persister + y-indexeddb bodies) is implemented; the durable replay half is not.
  • User-visible offline status at WRITE time: no 'queued/pending/offline' affordance on individual structural actions (create/move/comment) — they render optimistic success or no feedback with no indication the op is local-only. (The header-level >5s 'Offline — changes saved locally' / 'Syncing changes…' indicator exists for the Yjs body contour but not for REST structural ops.)
  • Offline fallback UI: no 'you are offline' / error screen for routes that cannot hydrate offline — un-hydratable routes render a pure-white blank instead of any fallback.
  • AI provider not configured in the test stand (env limitation, not WIP and not a bug): 'AI not configured' on the AI chat surface is expected for this environment.

False positives (убиты verifier-проходом) (5)

  • 'Body editor stays read-only until Hocuspocus connects+syncs once, so a never-synced page cannot be edited offline' (reported by offline-edit-body). Verifier verdict: SPECULATIVE. The code gate (page-editor.tsx showStatic) is real and by-design, but the claimed symptom is unreachable/not impactful: offline-WITHOUT-reload of an already-synced page stays editable (contenteditable=true), and offline-WITH-reload blanks the whole app before any read-only editor could appear (that path is the separate white-screen defect / WIP shell). The transient static read-only resolves to editable within the first online poll. No standalone defect, no data loss.
  • 'Page-TITLE offline edits sync fully to the server on reconnect' (the reconnect-sync half, reported by offline-structural-outbox). Verifier verdict: SPECULATIVE on the sync claim. The optimistic-offline + y-indexeddb persistence half IS verified (title shows in H1/breadcrumb/sidebar offline and survives a reload), and the online baseline title->server pipeline works. But the offline->reconnect server sync did not reproduce cleanly: one run produced a duplicated/concatenated server title under an abnormal reconnect, another never synced within 45s. Largely a simulation confound — HocuspocusProvider runs against a separate origin (:3001) that the same-origin SW does not proxy and does not auto-reconnect under Playwright set_offline. Not asserted as a deterministic defect; the title is a Yjs fragment (Contour A), not a REST op.
  • Transient 401s on /api/users/me and /api/auth/collab-token during the login->home transition (reported by recon). Expected pre-auth requests fired on the login screen before the session cookie is set; a clean already-authenticated reload shows zero >=400 and an explicit POST /api/users/me returns 200. ENV/expected, not a bug.
  • socket.io WebSocket 'closed before the connection is established' warning during the login-page phase (reported by recon). Appears only before auth/navigation settles; the clean authenticated reload produces zero warnings. Cosmetic/transient, not a bug.
  • 'Offline navigation to /home serves precached shell but empty body' (reported by update-prompt-install as a standalone finding). Verifier verdict: SPECULATIVE/inconclusive as a separate item — it is the same auth-gate white-screen already captured as the high offline-sync defect, not a distinct bug.

Подробный мета-отчёт

QA Synthesis — PR #120 feature/offline-sync (Gitmost / Docmost fork, OFFLINE + PWA, WIP M0–M2)

1. Stand & method

  • Live PROD build at http://localhost:3000 (server-served SPA, service worker active: /sw.js, /manifest.json, 236 precached assets, registerType prompt, API/collab/socket.io = NetworkOnly).
  • Offline simulated correctly: single persistent chromium context → log in → wait navigator.serviceWorker.ready + controller != nullcontext.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

Stage Agent Coverage Outcome
Recon recon SW/IndexedDB baseline, login transition, offline UI surface map (make-offline tree action, header sync indicator, PWA update prompt) Clean baseline; confirmed SW activates+controls and RQ cache persists. 2 cosmetic env items (login 401s, socket.io warning).
Slice pwa-sw-shell Cold offline relaunch of authed routes; pinned-page offline read; pageerror capture Found the headline HIGH white-screen + pinned-page unreadable + the collab-token TypeError.
Slice offline-read Offline read of cached pages; positive data-persistence control; bootstrap crash Independently reproduced white-screen + collab-token TypeError; proved the cache IS populated (positive control) so the white-screen is a render-gate defect, not missing WIP.
Slice offline-edit-body Contour A (Yjs body) offline edit→reconnect→server; non-pinned offline reload Contour A VERIFIED working; non-pinned offline reload = blank (WIP); read-only-until-sync gate (later downgraded).
Slice offline-structural-outbox Contour B REST writes offline: create/move/comment/title Found the 3 silent-data-loss-on-reload defects + the create-UX defect; confirmed title is Yjs (works).
Slice update-prompt-install PWA install criteria, update-prompt semantics, /api NetworkOnly, offline /home 3 no-defect confirmations (installable, prompt-not-autoupdate, NetworkOnly) + re-found the collab-token TypeError.
Sanity online-sanity-regression Online regression sweep Found pre-existing collab-token 5/min throttle breaking the 6th page.
Verify doer-verifier pass Independent reproduction of every finding Killed false positives, corrected 2 mis-attributed root causes, downgraded 2 to speculative.

3. Real-vs-false tally

  • 8 confirmed real defects (6 offline-sync + 2 gitmost-ui), after dedup. The white-screen was reported 4×, the collab-token TypeError 3× → merged.
  • 5 confirmed no-defect / positive controls (PWA installable, update-prompt prompt-semantics, /api NetworkOnly, Contour-A body edit works, offline data persistence works).
  • 5 false-positives / reclassified (read-only-until-sync, title reconnect-sync, login 401s, socket.io warning, the duplicate /home-empty item).
  • 5 WIP/coverage-boundary items (not bugs).

4. Who-found-what (unique credit)

  • White-screen auth-gate (HIGH): pwa-sw-shell + offline-read (independent).
  • Pinned-page unreadable (MED): pwa-sw-shell.
  • collab-token retry TypeError (MED): offline-read, pwa-sw-shell, update-prompt-install (triple-found).
  • CREATE/MOVE/COMMENT silent-loss-on-reload (HIGH/HIGH/MED) + CREATE-UX (MED): offline-structural-outbox.
  • Collab-token throttle (MED, pre-existing): online-sanity-regression.

5. Offline coverage boundary (what actually works end-to-end vs not)

WORKS (verified):

  • Contour A — document BODY via Yjs + y-indexeddb: offline paragraph/heading edits survive reconnect and reach server Yjs state, confirmed from a clean server-only context (no conflict markers). Page TITLE (also a Yjs fragment) edits optimistically offline and persists via y-indexeddb.
  • PWA shell: installable manifest + controlling SW; Workbox precache serves the app shell offline; /api correctly NetworkOnly (no stale auth served); update prompt uses correct prompt semantics with no false nags.
  • Caching infra: React Query persister writes structural roots (pages/space/spaces/root-sidebar-pages/recent-changes/comments) to IndexedDB; make-offline warms even more. The DATA is there.

BROKEN (defects — the caching exists but is defeated):

  • Offline reload/relaunch of every authenticated route white-screens (one-line auth gate on an un-persisted currentUser query) — defeats ALL cached/pinned offline reading. This is the dominant issue: a ~3-line fix unlocks the whole offline-read feature.
  • collab-token retry throws an uncaught TypeError on every offline boot (one-char fix: optional chaining).

SILENT DATA LOSS (defects — false durability):

  • Offline structural writes (create/move/comment) are in-memory paused mutations: they show optimistic success / clear the input and replay if the tab stays open, but a reload/close while offline drops them with zero signal. Per the brief's rule ("claims success then silently loses data IS a bug") these count as defects, not WIP.

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)

  1. Persist/fallback the currentUser query — add 'currentUser' to OFFLINE_PERSIST_ROOTS + warm it in make-offline, and/or give UserProvider a cached placeholderData/last-known-user path so if (error) return <></> doesn't blank offline. Highest leverage: fixes both the HIGH white-screen and the pinned-page MED in one shot.
  2. auth-query.tsx:27error.response?.status === 404 — one-line fix removes the uncaught TypeError + retry/backoff corruption on every offline boot.
  3. Wire the offline outbox (M2): add mutationKeys + setMutationDefaults (with default mutationFns) so resumePausedMutations can replay dehydrated paused mutations after reload; consider networkMode:'offlineFirst'. This converts the three silent-data-loss defects into durable queued writes.
  4. Write-time offline feedback: show a 'queued/offline' state on create/move/comment immediately (don't await the paused mutation before the optimistic insert/navigate); prevents duplicate-create-on-reclick and the jarring delayed navigation.
  5. Offline fallback UI: render a 'You're offline' screen (or last cached view) for un-hydratable routes instead of a white blank.
  6. Pre-existing: exempt collab-token from 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

  • Headless SW/install limits: real OS-level PWA install and a true process-kill cold relaunch can't be exercised headless; install criteria and SW-controlled offline reload were validated, but standalone-window behavior was not.
  • Cross-origin collab (:3001): HocuspocusProvider talks to a separate origin the same-origin SW doesn't proxy and doesn't auto-reconnect under Playwright 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.
  • Rate limiting (429): the collab-token 5/min throttle and login rate limits made parallel-agent runs flaky; some read-only-until-sync observations were under 429 storms, not pure offline.
  • AI not configured: env limitation; AI chat surface untested for content, not counted against the PR.

🤖 web-test-orchestrator (multi-agent QA), прогон по PR #120

Автономное тестирование **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/<space>/p/<page>, /settings/...). Observe a solid-white page; contrast: in the SAME offline context /login renders the full form and /p/<bogus> renders Error404, so the SW-served shell is healthy — only <Layout>-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/<cached page>. 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.<uuid> 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/<pinnedPageSlug>. **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:24 `if (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) - Offline cold-read of pages NOT opened/pinned this session (arbitrary Contour-B structural offline): reloading or navigating to a non-cached page while offline yields a blank shell with no content — the structural offline shell (M1/M2) is only partially built. Not a bug per se (this was never the headline Contour-A guarantee), but it is the coverage boundary. Note the failure mode oscillates between blank-white-shell and a redirect to the cached /login form. - Durable offline write OUTBOX / mutation-queue (M2): there is no setMutationDefaults, no MutationCache wiring, no mutationKeys, no onlineManager/networkMode config. Offline structural mutations (create/move/comment/etc.) only survive as in-memory paused React Query mutations for the life of the tab; nothing replays them after a reload/close. The persistence half (read queries via query-persister + y-indexeddb bodies) is implemented; the durable replay half is not. - User-visible offline status at WRITE time: no 'queued/pending/offline' affordance on individual structural actions (create/move/comment) — they render optimistic success or no feedback with no indication the op is local-only. (The header-level >5s 'Offline — changes saved locally' / 'Syncing changes…' indicator exists for the Yjs body contour but not for REST structural ops.) - Offline fallback UI: no 'you are offline' / error screen for routes that cannot hydrate offline — un-hydratable routes render a pure-white blank instead of any fallback. - AI provider not configured in the test stand (env limitation, not WIP and not a bug): 'AI not configured' on the AI chat surface is expected for this environment. ## ✅ False positives (убиты verifier-проходом) (5) - 'Body editor stays read-only until Hocuspocus connects+syncs once, so a never-synced page cannot be edited offline' (reported by offline-edit-body). Verifier verdict: SPECULATIVE. The code gate (page-editor.tsx showStatic) is real and by-design, but the claimed symptom is unreachable/not impactful: offline-WITHOUT-reload of an already-synced page stays editable (contenteditable=true), and offline-WITH-reload blanks the whole app before any read-only editor could appear (that path is the separate white-screen defect / WIP shell). The transient static read-only resolves to editable within the first online poll. No standalone defect, no data loss. - 'Page-TITLE offline edits sync fully to the server on reconnect' (the reconnect-sync half, reported by offline-structural-outbox). Verifier verdict: SPECULATIVE on the sync claim. The optimistic-offline + y-indexeddb persistence half IS verified (title shows in H1/breadcrumb/sidebar offline and survives a reload), and the online baseline title->server pipeline works. But the offline->reconnect server sync did not reproduce cleanly: one run produced a duplicated/concatenated server title under an abnormal reconnect, another never synced within 45s. Largely a simulation confound — HocuspocusProvider runs against a separate origin (:3001) that the same-origin SW does not proxy and does not auto-reconnect under Playwright set_offline. Not asserted as a deterministic defect; the title is a Yjs fragment (Contour A), not a REST op. - Transient 401s on /api/users/me and /api/auth/collab-token during the login->home transition (reported by recon). Expected pre-auth requests fired on the login screen before the session cookie is set; a clean already-authenticated reload shows zero >=400 and an explicit POST /api/users/me returns 200. ENV/expected, not a bug. - socket.io WebSocket 'closed before the connection is established' warning during the login-page phase (reported by recon). Appears only before auth/navigation settles; the clean authenticated reload produces zero warnings. Cosmetic/transient, not a bug. - 'Offline navigation to /home serves precached shell but empty body' (reported by update-prompt-install as a standalone finding). Verifier verdict: SPECULATIVE/inconclusive as a separate item — it is the same auth-gate white-screen already captured as the high offline-sync defect, not a distinct bug. --- # Подробный мета-отчёт # QA Synthesis — PR #120 `feature/offline-sync` (Gitmost / Docmost fork, OFFLINE + PWA, WIP M0–M2) ## 1. Stand & method - Live PROD build at http://localhost:3000 (server-served SPA, **service worker active**: /sw.js, /manifest.json, 236 precached assets, registerType `prompt`, API/collab/socket.io = NetworkOnly). - Offline simulated correctly: single persistent chromium context → log in → wait `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 | Stage | Agent | Coverage | Outcome | |---|---|---|---| | Recon | `recon` | SW/IndexedDB baseline, login transition, offline UI surface map (make-offline tree action, header sync indicator, PWA update prompt) | Clean baseline; confirmed SW activates+controls and RQ cache persists. 2 cosmetic env items (login 401s, socket.io warning). | | Slice | `pwa-sw-shell` | Cold offline relaunch of authed routes; pinned-page offline read; pageerror capture | Found the **headline HIGH white-screen** + pinned-page unreadable + the collab-token TypeError. | | Slice | `offline-read` | Offline read of cached pages; positive data-persistence control; bootstrap crash | Independently reproduced white-screen + collab-token TypeError; proved the cache IS populated (positive control) so the white-screen is a render-gate defect, not missing WIP. | | Slice | `offline-edit-body` | Contour A (Yjs body) offline edit→reconnect→server; non-pinned offline reload | **Contour A VERIFIED working**; non-pinned offline reload = blank (WIP); read-only-until-sync gate (later downgraded). | | Slice | `offline-structural-outbox` | Contour B REST writes offline: create/move/comment/title | Found the **3 silent-data-loss-on-reload defects** + the create-UX defect; confirmed title is Yjs (works). | | Slice | `update-prompt-install` | PWA install criteria, update-prompt semantics, /api NetworkOnly, offline /home | 3 **no-defect** confirmations (installable, prompt-not-autoupdate, NetworkOnly) + re-found the collab-token TypeError. | | Sanity | `online-sanity-regression` | Online regression sweep | Found pre-existing collab-token 5/min throttle breaking the 6th page. | | Verify | doer-verifier pass | Independent reproduction of every finding | Killed false positives, corrected 2 mis-attributed root causes, downgraded 2 to speculative. | ## 3. Real-vs-false tally - **8 confirmed real defects** (6 offline-sync + 2 gitmost-ui), after dedup. The white-screen was reported 4×, the collab-token TypeError 3× → merged. - **5 confirmed no-defect / positive controls** (PWA installable, update-prompt prompt-semantics, /api NetworkOnly, Contour-A body edit works, offline data persistence works). - **5 false-positives / reclassified** (read-only-until-sync, title reconnect-sync, login 401s, socket.io warning, the duplicate /home-empty item). - **5 WIP/coverage-boundary items** (not bugs). ## 4. Who-found-what (unique credit) - White-screen auth-gate (HIGH): `pwa-sw-shell` + `offline-read` (independent). - Pinned-page unreadable (MED): `pwa-sw-shell`. - collab-token retry TypeError (MED): `offline-read`, `pwa-sw-shell`, `update-prompt-install` (triple-found). - CREATE/MOVE/COMMENT silent-loss-on-reload (HIGH/HIGH/MED) + CREATE-UX (MED): `offline-structural-outbox`. - Collab-token throttle (MED, pre-existing): `online-sanity-regression`. ## 5. Offline coverage boundary (what actually works end-to-end vs not) **WORKS (verified):** - Contour A — document BODY via Yjs + y-indexeddb: offline paragraph/heading edits survive reconnect and reach server Yjs state, confirmed from a clean server-only context (no conflict markers). Page TITLE (also a Yjs fragment) edits optimistically offline and persists via y-indexeddb. - PWA shell: installable manifest + controlling SW; Workbox precache serves the app shell offline; /api correctly NetworkOnly (no stale auth served); update prompt uses correct `prompt` semantics with no false nags. - Caching infra: React Query persister writes structural roots (pages/space/spaces/root-sidebar-pages/recent-changes/comments) to IndexedDB; make-offline warms even more. The DATA is there. **BROKEN (defects — the caching exists but is defeated):** - Offline reload/relaunch of every authenticated route white-screens (one-line auth gate on an un-persisted `currentUser` query) — defeats ALL cached/pinned offline reading. This is the dominant issue: a ~3-line fix unlocks the whole offline-read feature. - collab-token retry throws an uncaught TypeError on every offline boot (one-char fix: optional chaining). **SILENT DATA LOSS (defects — false durability):** - Offline structural writes (create/move/comment) are in-memory paused mutations: they show optimistic success / clear the input and replay if the tab stays open, but a reload/close while offline drops them with zero signal. Per the brief's rule ("claims success then silently loses data IS a bug") these count as defects, not WIP. **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) 1. **Persist/fallback the `currentUser` query** — add `'currentUser'` to OFFLINE_PERSIST_ROOTS + warm it in make-offline, and/or give UserProvider a cached `placeholderData`/last-known-user path so `if (error) return <></>` doesn't blank offline. Highest leverage: fixes both the HIGH white-screen and the pinned-page MED in one shot. 2. **`auth-query.tsx:27` → `error.response?.status === 404`** — one-line fix removes the uncaught TypeError + retry/backoff corruption on every offline boot. 3. **Wire the offline outbox (M2):** add `mutationKey`s + `setMutationDefaults` (with default mutationFns) so `resumePausedMutations` can replay dehydrated paused mutations after reload; consider `networkMode:'offlineFirst'`. This converts the three silent-data-loss defects into durable queued writes. 4. **Write-time offline feedback:** show a 'queued/offline' state on create/move/comment immediately (don't await the paused mutation before the optimistic insert/navigate); prevents duplicate-create-on-reclick and the jarring delayed navigation. 5. **Offline fallback UI:** render a 'You're offline' screen (or last cached view) for un-hydratable routes instead of a white blank. 6. **Pre-existing:** exempt `collab-token` from 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 - **Headless SW/install limits:** real OS-level PWA install and a true process-kill cold relaunch can't be exercised headless; install *criteria* and SW-controlled offline reload were validated, but standalone-window behavior was not. - **Cross-origin collab (:3001):** HocuspocusProvider talks to a separate origin the same-origin SW doesn't proxy and doesn't auto-reconnect under Playwright `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. - **Rate limiting (429):** the collab-token 5/min throttle and login rate limits made parallel-agent runs flaky; some read-only-until-sync observations were under 429 storms, not pure offline. - **AI not configured:** env limitation; AI chat surface untested for content, not counted against the PR. 🤖 web-test-orchestrator (multi-agent QA), прогон по PR #120
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#220