Skip to content

fix(storage): make factory-reset recovery deterministic (#591, #593) - #592

Open
qnbs wants to merge 2 commits into
mainfrom
fix/591-factory-reset-url-sanitization
Open

fix(storage): make factory-reset recovery deterministic (#591, #593)#592
qnbs wants to merge 2 commits into
mainfrom
fix/591-factory-reset-url-sanitization

Conversation

@qnbs

@qnbs qnbs commented Sep 3, 2026

Copy link
Copy Markdown
Owner

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 #/settings into the URL hash 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 wipe could still reboot straight back into Settings.

Fix: sanitizeViewCarryingUrlState() strips the hash and the view query param via history.replaceState immediately 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'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 Redux state.
  • Settings appears to survive (a write far enough along to commit before 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 evaluate false and skip the WelcomePortal, landing on the Dashboard instead, where useProjectBootstrapEffect then 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 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.

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 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. Verified against URL/URLSearchParams semantics directly (not assumed) before implementing.

Non-goals

Test plan

  • pnpm run lint — pass
  • pnpm 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 fix
  • pnpm run ci:prepush — pass
  • Full GitHub CI, including a genuine first-attempt, zero-Playwright-retry Chromium + Mobile Chrome pass on onboarding-entry-precondition.spec.ts (no rerun-only acceptance, per this repo's standing bar for E2E-nondeterminism fixes)

Summary by CodeRabbit

  • Bug Fixes

    • Factory reset now clears view-specific URL state before reloading while preserving unrelated URL parameters.
    • Failed reset operations no longer reload the application and correctly report the failure.
    • Persistence is paused while a factory reset is in progress, preventing state from being saved during reset-related reloads.
  • Documentation

    • Updated README test metrics to reflect 7,361+ tests across 595 files.
  • Tests

    • Added coverage for reset progress, failed resets, URL sanitization, and paused persistence during factory reset.

…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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @qnbs, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 5 days and 11 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@codeant-ai

codeant-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed 7d463f7 Sep 03, 2026 · 06:20 06:21
✅ Reviewed your PR ca6870a Sep 03, 2026 · 02:24 02:27

@codeant-ai

codeant-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
worldscript-studio Ready Ready Preview Sep 3, 2026 6:21am UTC

@sourcery-ai

sourcery-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

The 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 reload

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Sanitize persisted URL view state immediately before the factory-reset reload.
  • Add a defensive URL sanitizer that removes the hash and view query parameter while preserving path and unrelated query parameters.
  • Invoke sanitization after reset cleanup and delay, directly before window.location.reload().
  • Keep sanitization failures from blocking the reset flow.
services/factoryResetService.ts
Add regression coverage for reset URL sanitization and update documented test counts.
  • Verify hash and view state are removed, unrelated URL state is retained, and history replacement precedes reload.
  • Update README test-count references for the added test.
  • Apply formatting-only changes to the cache test and owned-cache regex.
tests/unit/factoryResetService.test.ts
README.md

Assessment against linked issues

Issue Objective Addressed Explanation
#591 Ensure that a factory reset does not preserve the active view's hash or view query parameter when reloading, so a successful reset boots into the WelcomePortal rather than the pre-reset view.
#591 Preserve unrelated URL state and avoid changing normal deep-link handling outside the factory-reset flow.
#591 Add regression coverage verifying URL sanitization occurs before the factory-reset reload.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@what-the-diff

what-the-diff Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR Summary

  • Updating Test Count Details in README
    The count of tests reflected in the README document has been increased slightly from its previous number. This new value is also being updated in various sections within the document.

  • New Function for Sanitizing URL
    A new function, sanitizeViewCarryingUrlState, has been added that cleans the URL before the application reloads. It specifically removes certain parts from the URL like the view query parameter and hash.

  • Enhanced Data Wiping Function
    Updates have been made to the wipeAllAppData function to utilize the new sanitizeViewCarryingUrlState function before the application reloads. This ensures that URL is cleaned properly, preventing redundancy.

  • Testing for New Function
    A test case has been added to verify the correct operation of the new function sanitizeViewCarryingUrlState. The test ensures that the function efficiently removes the view parameter while retaining the necessary query parameters in the URL.

  • Improved Readability in Coding
    For improved readability, the regular expression constants used in the factoryResetService.ts file have been restructured. This has been achieved by separating the expressions into various lines rather than having them in a single, potentially confusing, line.

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Sep 3, 2026
@deepsource-io

deepsource-io Bot commented Sep 3, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in e81e6c8...7d463f7 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

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-ai

codeant-ai Bot commented Sep 3, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 7d463f71
Scan Time: 2026-09-03 06:23:34 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues

View Full Results

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and view query 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.

codescene-access[bot]

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Essentials

Run ID: 9d3eb656-f4e7-4d45-8bc2-779fecfd9875

📥 Commits

Reviewing files that changed from the base of the PR and between ca6870a and 7d463f7.

📒 Files selected for processing (5)
  • README.md
  • app/persistedStateFlush.ts
  • services/factoryResetService.ts
  • tests/unit/factoryResetService.test.ts
  • tests/unit/persistedStateFlush.test.ts
 ___________________________________________________________________
< This refactor is like rearranging furniture during an earthquake. >
 -------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Essentials

Run ID: c90bb4ed-6d22-4202-966c-17495b353cf4

📥 Commits

Reviewing files that changed from the base of the PR and between e81e6c8 and ca6870a.

📒 Files selected for processing (3)
  • README.md
  • services/factoryResetService.ts
  • tests/unit/factoryResetService.test.ts

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.


📝 Walkthrough

Walkthrough

Factory 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.

Changes

Factory reset URL handling

Layer / File(s) Summary
Reset URL sanitization
services/factoryResetService.ts, tests/unit/factoryResetService.test.ts
Factory reset removes the hash and view query parameter before reload. Other URL parameters remain. Tests verify the history update and call order.

README test metrics

Layer / File(s) Summary
Update test metrics
README.md
README badges, testing details, project structure, and CI metrics now report 7,358+ tests across 595 test files.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to ca687

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
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The factory-reset changes are in scope for #591, but the README test-metric update is unrelated to the issue objectives. Remove the unrelated README.md test-metric changes, or provide a linked requirement that justifies including them in this pull request.
Docstring Coverage ⚠️ Warning 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… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: sanitizing view-carrying URL state before the factory-reset reload.
Linked Issues check ✅ Passed The implementation satisfies issue #591. It removes the hash and view query parameter before reload, preserves unrelated URL state, leaves normal deep-link priority unchanged, and adds regression cove…
Full details: Linked Issues check

Explanation

The implementation satisfies issue #591. It removes the hash and view query parameter before reload, preserves unrelated URL state, leaves normal deep-link priority unchanged, and adds regression coverage.

Full details: Docstring Coverage

Explanation

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.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/591-factory-reset-url-sanitization

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread services/factoryResetService.ts Outdated
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

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.
@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Sep 3, 2026
@qnbs qnbs changed the title fix(storage): sanitize view-carrying URL state before factory-reset reload fix(storage): make factory-reset recovery deterministic (#591, #593) Sep 3, 2026

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(e2e): factory-reset reload can boot back into the last-active view instead of the WelcomePortal

1 participant