fix(storage): make factory-reset recovery deterministic (#591, #593) - #592
fix(storage): make factory-reset recovery deterministic (#591, #593)#592qnbs wants to merge 2 commits into
Conversation
…eload Fixes #591. ensureWelcomePortalEntry()'s Factory-Reset recovery flow (PR #590) necessarily navigates to Settings before triggering the reset, which writes #/settings into the URL via pushHash(). wipeAllAppData()'s final window.location.reload() preserves that same URL, and useApp.ts's readInitialView() reads the hash (then the 'view' query param) with higher priority than checking whether a project even exists -- so a genuinely successful data wipe can still reboot straight back into the pre-reset view instead of the WelcomePortal. Root-caused via the actual Playwright trace/accessibility-snapshot artifacts from two independent CI runs: the console-log timeline proved the wipe itself succeeded (no persisted-project rehydration message after reload), ruling out an IDB-deletion race -- confirmed separate from and unrelated to #589 and to PR #583's IDB reset-gate work (neither hooks/useApp.ts nor services/deepLinkService.ts is touched by #583). sanitizeViewCarryingUrlState() strips the hash and the 'view' query param via history.replaceState immediately before the real reload, preserving unrelated query/path state and the existing reload timing. Does not touch normal deep-link priority for ordinary navigation.
🤖 CodeAnt AI — Review Status
|
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideThe factory-reset flow now removes deep-link state that would otherwise survive a full reload and take a successfully wiped app back to its pre-reset view, with focused ordering and preservation regression coverage. Sequence diagram for factory-reset URL sanitization before reloadsequenceDiagram
participant Settings
participant FactoryReset as factoryResetService
participant Browser
participant App as useApp
Settings->>FactoryReset: wipeAllAppData()
FactoryReset->>FactoryReset: sanitizeViewCarryingUrlState()
FactoryReset->>Browser: history.replaceState(path + unrelated query)
FactoryReset->>Browser: window.location.reload()
Browser->>App: readInitialView()
App-->>Browser: show WelcomePortal
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
PR Summary
|
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| Docker | Sep 3, 2026 6:20a.m. | Review ↗ | |
| Python | Sep 3, 2026 6:20a.m. | Review ↗ | |
| Rust | Sep 3, 2026 6:20a.m. | Review ↗ | |
| Shell | Sep 3, 2026 6:20a.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
🏁 CodeAnt Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
There was a problem hiding this comment.
This PR correctly fixes the factory-reset URL state bug described in issue #591. The implementation is clean and well-tested.
Changes Reviewed:
- factoryResetService.ts: Added
sanitizeViewCarryingUrlState()to strip hash andviewquery parameter before reload, fixing the bug where reset would redirect back to the pre-reset view - factoryResetService.test.ts: Added comprehensive regression test verifying URL sanitization happens before reload and preserves unrelated URL state
- README.md: Updated test count badges (7357+ → 7358+)
Strengths:
- The fix is correctly positioned in the execution flow (after IDB/cache clearing, before reload)
- Test coverage includes call order verification to ensure sanitization precedes reload
- Edge cases are properly handled (try-catch prevents URL sanitization from blocking reset)
- The regex-based cache filtering and Tauri data clearing remain unchanged and correct
No blocking issues found. The implementation aligns with the PR description and successfully addresses the root cause where readInitialView() reads URL state before checking project existence.
You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (5)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (3)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthroughFactory reset now removes view-carrying URL state before reload. Tests verify hash and query sanitization while preserving unrelated parameters. README metrics now report 7,358+ tests. ChangesFactory reset URL handling
README test metrics
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Factory reset now removes stale view-routing URL state before reload, preventing users from returning to the prior view after their data is cleared while preserving unrelated URL parameters. The targeted behavior is covered and no merge-blocking risk remains. Sequence Diagram(s)sequenceDiagram
participant wipeAllAppData
participant sanitizeViewCarryingUrlState
participant window.history
participant window.location
wipeAllAppData->>sanitizeViewCarryingUrlState: sanitize URL state
sanitizeViewCarryingUrlState->>window.history: replaceState with preserved path and query
wipeAllAppData->>window.location: reload page
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation The implementation satisfies issue Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…nd stop reserializing unrelated query state Two fixes, both discovered during #592's own validation: 1. services/factoryResetService.ts: url.searchParams.delete('view') + reading url.search back reserializes every retained query parameter via URLSearchParams.toString(), not just the one being removed -- e.g. turning a raw %20 into +, or a bare flag ?foo into ?foo=. Replaced with a string-level stripViewQueryParam() that removes only the view key, leaving every other parameter's raw encoding untouched. (Valid Cubic P3 finding on PR #592.) 2. Fixes #593. index.tsx's visibilitychange handler (and the desktop quit-flush, and register-sw.ts's update flush -- all three funnel through flushPersistedState()) fires on window.location.reload() itself, since a reload triggers visibilitychange before the page actually unloads. wipeAllAppData() doesn't stop the running app or its listeners during the 300ms settle window before that reload, so this flush can reopen and repopulate the IndexedDB database it just deleted with the stale, pre-reset in-memory state -- settings appear to reappear (a write far enough along to survive the unload) while the project usually doesn't (interrupted first, later in the same Promise.allSettled), producing exactly the 'settings-only persisted state' shape that makes index.tsx's isNewUser = !preloadedState false and skips the WelcomePortal. Confirmed via trace/console-log evidence: no project-rehydration log after the reset-triggered reload (ruling out an IDB-deletion race), yet the app boots into the Dashboard with a synthetically-seeded placeholder project -- exactly what useProjectBootstrapEffect produces once isPortalActive is (wrongly) false, which only happens if some persisted state, even settings-only, was found. isFactoryResetInProgress() (factoryResetService.ts) is set before any wipe work starts and guards flushPersistedState() itself, so all three call sites are protected by one change. Resets back to false if the reset itself fails and never reloads, so a failed attempt doesn't silently block every future save for the rest of the session. Confirmed independent of PR #583's IDB reset-gate architecture in mechanism (this closes one specific persistence-during-reset race with a minimal flag, not the general-purpose admission/generation/fail- closed gate #583 builds for every long-lived connection) but the same class of problem -- when #583 rebases, this invariant needs to be preserved inside its hardened reset implementation, not reintroduced separately.
There was a problem hiding this comment.
Gates Passed
3 Quality Gates Passed
See analysis details in CodeScene
Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
Summary
Fixes #591 and #593 — two real, independent bugs in
ensureWelcomePortalEntry()'s Factory-Reset recovery flow, both root-caused via the actual Playwright trace/accessibility-snapshot/console-log artifacts from CI runs during this PR's own convergence.Bug 1 — #591: stale view-carrying URL survives the reset reload
ensureWelcomePortalEntry()'s Factory-Reset recovery flow necessarily navigates to Settings before triggering the reset. Ordinary in-app navigation writes#/settingsinto the URL hash viapushHash().wipeAllAppData()'s finalwindow.location.reload()preserves that same URL, anduseApp.ts'sreadInitialView()reads the hash (then theviewquery param) with higher priority than checking whether a project even exists — so a genuinely successful wipe could still reboot straight back into Settings.Fix:
sanitizeViewCarryingUrlState()strips the hash and theviewquery param viahistory.replaceStateimmediately before the real reload. (A follow-up Cubic finding on this fix was also addressed — see below.)Bug 2 — #593: visibilitychange flush races the reset's own reload
After #591's fix, a different symptom appeared at the same final assertion: the app landed on the Dashboard with a synthetically-seeded placeholder project instead of the WelcomePortal. Traced via console-log timeline evidence (no project-rehydration message after the reset+reload, ruling out an IDB-deletion race) plus source tracing of
index.tsx's boot sequence:index.tsx'svisibilitychangehandler (and the desktop quit-flush, andregister-sw.ts's update flush — all three funnel throughflushPersistedState()) fires onwindow.location.reload()itself, since a reload triggersvisibilitychangebefore the page actually unloads.wipeAllAppData()doesn't stop the running app or its listeners during the 300ms settle window before that reload, so this flush can reopen and repopulate the IndexedDB database it just deleted with the stale, pre-reset in-memory Redux state.Promise.allSettled) — producing exactly the "settings-only persisted state" shape that makesindex.tsx'sisNewUser = !preloadedStateevaluatefalseand skip the WelcomePortal, landing on the Dashboard instead, whereuseProjectBootstrapEffectthen seeds a placeholder project title into the always-non-null default Redux project shell.Confirmed independent of PR #583's IDB reset-gate architecture in mechanism (this closes one specific persistence-during-reset race with a minimal flag; #583 builds a general-purpose admission/generation/fail-closed gate for every long-lived connection) but the same class of problem — when #583 rebases, this invariant needs to be preserved inside its hardened reset implementation, not reintroduced separately.
Fix:
isFactoryResetInProgress()is set before any wipe work starts and guardsflushPersistedState()itself, so all three call sites are protected by one change. Resets back tofalseif the reset itself fails and never reloads, so a failed attempt doesn't silently block every future save for the rest of the session.Bug 3 (review finding) — reserialization of unrelated query state
A Cubic P3 finding on the original #591 fix was valid:
url.searchParams.delete('view')followed by readingurl.searchback reserializes every retained query parameter viaURLSearchParams.toString(), not just the one being removed — e.g. turning a raw%20into+, or a bare flag?foointo?foo=. Replaced with a string-levelstripViewQueryParam()that removes only theviewkey, leaving every other parameter's raw encoding untouched. Verified againstURL/URLSearchParamssemantics directly (not assumed) before implementing.Non-goals
hooks/useApp.ts's deep-link priority order orservices/deepLinkService.ts.Test plan
pnpm run lint— passpnpm run typecheck— pass (exact CI command)pnpm exec vitest run tests/unit/factoryResetService.test.ts tests/unit/persistedStateFlush.test.ts tests/unit/registerSwUpdateFlush.test.ts— 27/27 pass, including new regression tests for both bugs and the query-encoding fixpnpm run ci:prepush— passonboarding-entry-precondition.spec.ts(no rerun-only acceptance, per this repo's standing bar for E2E-nondeterminism fixes)Summary by CodeRabbit
Bug Fixes
Documentation
Tests