Repository navigation
UN-4271 [FEAT] Frontend v1 version switch and route registry - #2433
Conversation
Typed route registry for v1 pages (declare in PAGES, go live with one line in REGISTERED_PAGES), validated by a unit test that fails CI on overlapping patterns, unmapped v0 routes, missing components or a toggle outside Human Review (D14). VersionSwitch wraps v0's routes in Router.jsx: unregistered URLs render v0 unchanged, registered pages render v1 as their own lazily loaded tree, and their v0 URLs redirect to the mapped v1 route. Human Review's old-version preference is per user, per browser (localStorage); setOldVersion() moves the user to the mapped screen without a reload. VITE_V1_DEV_ROUTES (set only for the E2E image in ci-test.yaml) registers demo pages that prove the switch and honours a preview override for declared-but-unregistered pages. resolveUrl() resolves a page's v1 route to the URL the user gets, for global search (UN-4278).
for more information, see https://pre-commit.ci
- Validation rejects a mapping whose URL lands on a route of the other version (an endless redirect), malformed page keys, and any v1 route without a toV0 mapping, so resolveUrl never returns null for a declared page's route. - Redirects and resolveUrl encode param values, so %2F, %3F and %23 stay inside their segment instead of becoming separators. - resolveUrl decides liveness by page key, not object identity. - An unknown VITE_UNSTRACT_DISTRIBUTION fails `vite build`; at runtime it logs and falls back to oss instead of blanking the app. - Registry validation runs only in development (CI covers release). - The v1 error boundary reloads once on a stale chunk (as v0 does) and offers a Reload button. - Dockerfile build args move below the install layer so CI and release share the cached install; VITE_UNSTRACT_DISTRIBUTION is settable. - Public helpers move to app/api.ts so v1 code can import them without the entry. - Tests now catch the guards mutation testing showed were unprotected (stale-user bounce, history replace, preview gating, distribution, error boundary, encoding); comments corrected.
Self-review (code, tests, error handling, types, comments)I fixed everything below in 217c586. Bugs
Tests
Comments: I corrected the inaccurate ones: Not changed
|
[Medium impact] The PR appears safe to merge; no new actionable issues were found.
|
…(UN-4271) - Validation rejects a v1 and a v0 route of the same page that match the same URL (identical patterns excepted). This covers the redirect loop the placeholder-URL check caught and the case it missed: a param equal to a static segment (/old/settings -> /new/settings -> /settings). - The demo review page encodes orgName in its links, so an encoded org name stays one segment. - Use a Set for the param lookup (Sonar S7776).
|
Hi Jaseem, this is from P2-T10 (Global search), which consumes Two things that would help consumers:
|
|
Follow-up from Global search (P2-T10, UN-4278), and it applies to the app shell rail (P2-T01) too.
Could the registry hold a v0 home URL per page key, e.g. the API deployments page → |
…h-route-registry # Conflicts: # frontend/src/v1/README.md
The feedback components from main say to mount the Toaster once inside the v1 root; V1App is that root. It sits inside ThemeProvider (toasts portal into the themed #v1-portal) and outside Suspense/Routes, so it stays mounted while a page loads and across v1 route changes. The VersionSwitch test checks notify() shows a toast on a v1 page and falls back to the console once the user moves to a v0 screen.
Deepak-Kesavan
left a comment
There was a problem hiding this comment.
🤖 Automated review
Automated review by Unstract PR review kit (Claude Code). These are starting points, not verdicts: the maintainer decides what to act on, closing any of them is a legitimate outcome, and nothing here blocks the merge. Each finding was checked by an independent verifier; anything tagged [unverified] had its facts confirmed but is a judgement call.
4 inline comment(s).
P0-T09 harness contract (#2355), checked against this head
- Registry importable outside Vite: yes.
registry/types.tsandregistry/entries.tsare plain data with relative imports, socontract/registry.tscan re-exportPAGES,DEMO_PAGES,REGISTERED_PAGESand the types. The PR'sPageKeyalso allowsDEMO-…, which #2355's own type doesn't. - Dev/test preview of an unregistered page: yes. It uses
{ kind: "localStorage", key: "unstract.v1.preview" }, holds a JSON array of declared keys, is read once at load, and is honoured only withVITE_V1_DEV_ROUTES=true. A cookie isn't needed, so the cookie encoding inpreviewValuedoesn't apply. The key can't be imported (see thelive.tscomment). - Preference key and demo routes: yes, with one difference. The key is per user,
unstract.v1.oldVersion.<userId>="true", not one fixed key. The helpers inlib/oldVersion.ts(oldVersionStorageKey,OLD_VERSION_ON,LAST_USER_STORAGE_KEY) can be imported. The demo routes are/:orgName/v1-demoand/:orgName/v1-demo-review(plus/details), and their v0 stand-ins are/:orgName/v0-demoand/:orgName/v0-demo-review. The E2E image enables them inci-test.yaml. Because of theVersionSwitch.tsxcomment, a round trip in an API-login context also needsunstract.v1.lastUserset. - v1 root marker: yes.
ThemeProvider, whichV1Appmounts, rendersdata-ui-version="v1"on the.v1root. That matchesV1_ROOT_SELECTORas it is.
Spec acceptance includes E2E for the redirect and the Human Review round trip. The PR body says those specs follow once #2355 merges, and #2355 is waiting on this PR. Noting the ordering here only so it's visible.
review-pr-bot:review
- resolvePageUrl/resolveUrl return null when a :param of the pattern is missing or empty, instead of generatePath throwing or dropping the segment. A final splat may still be omitted. - PREVIEW_STORAGE_KEY moves to registry/types.ts (re-exported from live.ts) so the E2E harness can import it without Vite; the types.ts header now lists lib/oldVersion.ts as harness-importable too. - checkEntry splits into small rule functions (SonarCloud S3776: cognitive complexity 21 > 15); the two mapping-coverage loops share one helper. Problems and their order are unchanged. - README and a test record that a load starting on a v1 URL has no userId until a v0 page loads (the session loads in v0's PersistentLogin), so the old-version toggle relies on unstract.v1.lastUser until P0-T06.
|
@ritwik-g thanks for checking this from the Search side.
|
vishnuszipstack
left a comment
There was a problem hiding this comment.
Review of PR 2433 — UN-4271 Frontend v1 version switch and route registry
Mode: INITIAL. Reviewed head e79814879873aa3049f0f4ea71c3c4b336fc32bb, merge base 119835db1ac02fbb69ce277f66d77c1eb216034a, base freshened (base_fetch: ok), 28 files, 2400 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
Signals: lenses 2, 4 and 13 raised (React lifecycle in VersionSwitch/V1App, an auth-adjacent change because a new subtree renders outside v0's guards, tests added), lenses 11 and 18 raised (Dockerfile build args, CI workflow).
Security deep-dive: done
boundary-sweep: 3 sinks traced, clean (the two console.error lines carry the build's own env value and a registry-problem list; the v1 fallback shows a fixed string).
Verdict — APPROVE WITH NITS
Summary — Critical: 0 · High: 0 · Medium: 4 · Low: 13 · Lenses run: 19/19 (1 not covered, 4 N/A; see checklist).
The design is sound and well tested where it matters: the v1 suite passes (52 files, 527 passed, 1 skipped on purpose, coverage 97% lines), typecheck:v1 and lint:v1 pass, and mutating the per-user masking, the toggle rule, the session fallback, the layout-effect sync, writeLastUser, replace and the sign-out guard each fails a test. The registry ships empty, so nothing below is reachable by users today. The Mediums are worth fixing before the first page registers.
Findings
Posted inline on the diff; index:
-
- [Medium] [Lens 2, 3] — The v1 error boundary never resets when navigating between routes of the same page —
frontend/src/v1/app/VersionSwitch.tsx:83
- [Medium] [Lens 2, 3] — The v1 error boundary never resets when navigating between routes of the same page —
-
- [Medium] [Lens 7, 11, 18] — No build passes
VITE_UNSTRACT_DISTRIBUTION, so cloud and on-prem images build asossand the distribution filter does nothing [ON-PREM UX] —docker/dockerfiles/frontend.Dockerfile:55
- [Medium] [Lens 7, 11, 18] — No build passes
-
- [Medium] [Lens 13] —
resolveUrl, the public link builder, has no test, and its docstring contradicts the switch during a user change —frontend/src/v1/app/api.ts:35
- [Medium] [Lens 13] —
-
- [Medium] [Lens 13] — The boundary's stale-chunk reload, the reason the boundary exists, is never exercised —
frontend/src/v1/app/VersionSwitch.tsx:60
- [Medium] [Lens 13] — The boundary's stale-chunk reload, the reason the boundary exists, is never exercised —
-
- [Low] [Lens 13, 16] — The Router wiring is untested, and the E2E flag ships with no E2E consumer —
frontend/src/routes/Router.jsx:174
- [Low] [Lens 13, 16] — The Router wiring is untested, and the E2E flag ships with no E2E consumer —
-
- [Low] [Lens 13] —
load()'s fallbacks are untested —frontend/src/v1/app/registry/live.ts:91
- [Low] [Lens 13] —
-
- [Low] [Lens 2] — Before the session loads, the previous user's "old version" choice redirects a different user, and the round trip loses the sub-route —
frontend/src/v1/app/VersionSwitch.tsx:113
- [Low] [Lens 2] — Before the session loads, the previous user's "old version" choice redirects a different user, and the round trip loses the sub-route —
-
- [Low] [Lens 2] — Registry patterns are matched ahead of all v0 routes, but validated only against each other —
frontend/src/v1/app/registry/resolve.ts:42
- [Low] [Lens 2] — Registry patterns are matched ahead of all v0 routes, but validated only against each other —
-
- [Low] [Lens 4] — Going live is described as a one-line change, but v1 renders outside
PersistentLoginandRequireAuth—frontend/src/v1/README.md:121
- [Low] [Lens 4] — Going live is described as a one-line change, but v1 renders outside
-
- [Low] [Lens 3] —
setOldVersionreturnstrueand says it persists even when the write failed —frontend/src/v1/stores/oldVersion.ts:55
- [Low] [Lens 3] —
-
- [Low] [Lens 2] — The screen is blank while the v1 chunk loads —
frontend/src/v1/app/VersionSwitch.tsx:84
- [Low] [Lens 2] — The screen is blank while the v1 chunk loads —
-
- [Low] [Lens 16] — Comments say v1's styles load only with a v1 page; with
cssCodeSplit: falsethey are in the global stylesheet —frontend/src/v1/app/V1App.tsx:3
- [Low] [Lens 16] — Comments say v1's styles load only with a v1 page; with
-
- [Low] [Lens 16] — The README says CI rejects a v1 and v0 route of one page matching the same URL; the validator allows identical ones —
frontend/src/v1/README.md:124
- [Low] [Lens 16] — The README says CI rejects a v1 and v0 route of one page matching the same URL; the validator allows identical ones —
-
- [Low] [Lens 16] — The Router comment says "registered"; the switch uses the wider live set —
frontend/src/routes/Router.jsx:176
- [Low] [Lens 16] — The Router comment says "registered"; the switch uses the wider live set —
-
- [Low] [Lens 7] — The old-version contract for the E2E harness needs two keys, but the hand-off names one —
frontend/src/v1/lib/oldVersion.ts:9
- [Low] [Lens 7] — The old-version contract for the E2E harness needs two keys, but the hand-off names one —
-
- [Low] [Lens 15] —
selectOldVersionis unused —frontend/src/v1/stores/oldVersion.ts:87
- [Low] [Lens 15] —
-
- [Low] [Lens 13] — Console spies are restored only when the test passes —
frontend/src/v1/app/VersionSwitch.test.tsx:217
- [Low] [Lens 13] — Console spies are restored only when the test passes —
Lens checklist
- Spec & intent — Clean
- Architectural fit & precedent — See findings #1, #7, #8, #11
- Correctness & edge cases — See findings #1, #10
- Security — See findings #9
- Data integrity & migrations — N/A — no server data or schema; the localStorage keys are covered under lens 7
- Concurrency — Clean
- API & contract compatibility — See findings #2, #15
- Reliability & resilience — Clean
- Performance & cost — Clean
- Observability — Clean
- Operational safety — See findings #2, #5
- LLM/agent-specific — N/A — no model, prompt or agent path
- Testing — See findings #3, #4, #5, #6, #17
- Dependencies & build — N/A — no dependency or lockfile change
- Code quality — See findings #16
- Doc & comment accuracy — See findings #12, #13, #14
- Cross-document rule consistency — Not covered — merge-result sweep unavailable: git 2.34 here, the script needs 2.38
- On-prem deployment experience — See findings #2
- Scaling & workload lifecycle — N/A — no worker or scaling config touched
Assessed by me rather than an agent: lenses 4, 5, 6, 7 (consumer search, with the type-design agent), 8, 11, 12, 14, 15 (pinned gate), 17, 18, 19.
- Lens 4: the deep-dive's find pass returned no candidates (redirect targets stay same-origin and re-encode params; the preview override is build-gated and validated; the preference is a UI flag).
- Lens 6: the only ordering hazard is the layout-effect user sync, and the lifecycle trace found its guard holds.
- Lens 8:
reloadOnceis bounded by a 10-second window in sessionStorage, so a missing chunk cannot reload-loop. - Lens 11: the Dockerfile ARGs sit below the install layer and default empty, and no release workflow sets
VITE_V1_DEV_ROUTES; the CI image isloaded, not pushed. - Lens 15:
biome cionsrc/v1,Router.jsxandvite.config.jsreports 0 errors and 5 warnings, all on lines that are pre-existing or only re-indented (verticalsRouter && verticalsRouter(), vite.config.js imports).typecheck:v1,lint:v1andtsc --noEmitpass. - Lens 17: the distribution list is stated twice (
vite.config.jsandtypes.tsDISTRIBUTIONS) and the two agree; the merge-result sweep could not run.
Open questions for the author
- Will the cloud and on-prem builds set
VITE_UNSTRACT_DISTRIBUTION, or does P0-T07 replace it before the first restricted entry (#2)? - Is P0-T06 sequenced before any go-live task (#9)?
- Should a second user on a shared browser be redirected by the first user's preference before their session loads (#7)?
- Is a splat pattern part of the #2355 harness contract?
patterns.tsaccepts a final*, and #2355'sfillPatterndoes not handle one.
Assumptions made
- The UN-4271 ticket and the Confluence spec were not opened; intent is judged from the PR body and the code.
- The cloud-repo workflows were read from the local
unstract-cloudcheckout (last commit 2026-11-02), not from its remote. - Behaviour was verified under happy-dom and by reading React Router 6.30 sources, not in a real browser.
cost: agents=8 wall=08:23 lenses-examined=14 of 19 files=28 lines=2400
- The v1 error boundary clears a failure when the pathname changes, as v0's backstop does with resetKeys; it stays keyed by page. - Rename VITE_UNSTRACT_DISTRIBUTION to VITE_DISTRIBUTION, the name P0-T07's plugin manifest (UN-4286, #2394) uses. README and Dockerfile say the cloud and on-prem builds do not set it yet. - Tests: resolveUrl (choice, last user, declared-not-live, undeclared), the boundary's stale-chunk reload and its ten-second window, the boundary reset, load()'s OSS fallback and DEV throw, a numeric and an empty user id. - README: registering before P0-T06 ships an unguarded page; the same-page overlap rule allows identical routes. - Docstrings and comments: setOldVersion may last for the session only; resolveUrl reads the store when called; v1 styles are in the global stylesheet (cssCodeSplit: false); Router says "live", names P0-T09. - Remove unused selectOldVersion; drop redundant vi.restoreAllMocks (the config sets restoreMocks); toHaveLength for the storage check (S5906).
Frontend Lint Report (Biome)✅ All checks passed! No linting or formatting issues found. |
|
@vishnuszipstack thanks for the review. Each finding is answered in its thread, and 850ebfa has the fixes. Your open questions:
|
|
Unstract test resultsPer-group results
Critical paths
|



What
src/v1/app/registry/): one entry per v1 page withpage,v1Routes,replaces,toV0,toV1,toggle,distributionsandregisteredBy. This is the contract table from the spec and the shape UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 expects. Pages are declared inPAGESand go live by adding their key toREGISTERED_PAGES, a one-line change (D9). The registry ships empty.validate.test.tsruns over the real registry and fails CI on:toV0mapping (soresolveUrlcan always link a declared page);toggleon any page other than UN11 (D14) and its demo stand-in.routes/Router.jsxwraps its routes inVersionSwitch(exported from@v1).app/V1App.tsx, with its own providers and error boundary. It is lazily loaded, so v1 code is fetched only when a v1 page renders.unstract.v1.oldVersion.<userId>. There is no backend call.setOldVersion(on)(exported from@v1for P6-T07's controls) moves the user to the mapped screen with no reload.resolveUrl(page, v1Pattern, params): returns the URL in the version the user gets, encoding raw param values. It is the interface asked for by P2-T10 global search (UN-4278).%2F,%3Fand%23are re-encoded, not turned into separators).VITE_V1_DEV_ROUTES=true, set only on the E2E image inci-test.yaml):Router.jsx.localStorage["unstract.v1.preview"] = '["UN05"]', which renders declared but unregistered pages as v1. UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 needs this for page tasks that test before go-live.Why
How
resolve.ts) is a pure function of the pathname, the live entries and the preference. It uses React Router'smatchRoutes, so the most specific pattern wins.Router.jsx. v0 loads the session inside its own routes, so on a fresh load the switch uses the last user signed in on this browser (unstract.v1.lastUser). A reload of a v1 Human Review URL therefore still lands on the old version. After a new user signs in, the previous user's choice never redirects them.registry/types.tsandregistry/entries.tsimport only each other and never readimport.meta.env, so the E2E harness can import them from Playwright. Environment flags are read inlive.ts.VITE_DISTRIBUTION(oss, the default,cloudoronprem) drops entries for distributions the build lacks. An unknown value failsvite build(vite.config.js); at runtime it logs and falls back toossrather than blanking the app. It is the name P0-T07's plugin manifest (UN-4286 [FEAT] Add the v1 plugin manifest, slots and build check #2394) uses. The cloud and on-prem builds don't set it yet, so every image builds asossuntil P0-T07 wires them.validate.test.ts), not in release builds, so its code stays out of the bundle v0 loads eagerly.app/api.ts, re-exported byindex.ts, so v1 code and tests never import the entry itself.routes/Router.jsxand build/CI configuration:vite.config.js, the frontend Dockerfile andci-test.yaml. The Dockerfile'sVITE_V1_DEV_ROUTESandVITE_DISTRIBUTIONbuild args are empty by default and declared below the install layer, so CI and release still share the cachedbun install.Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
cssCodeSplit: false, v1's stylesheet (P0-T04) is now merged into the global CSS. I diffed it against a build oforigin/main:.v1orui:, except duplicate@font-facedeclarations for the Inter and Geist Mono fonts v0 already loads.--tw-*property fallback for old browsers is also added.Database Migrations
Env Config
VITE_V1_DEV_ROUTES: build-time, optional.trueonly for E2E and local development.VITE_DISTRIBUTION: build-time, optional, defaults tooss; also a Dockerfile build arg. An unknown value fails the build.Relevant Docs
frontend/src/v1/README.md, new section "Version switch and route registry"Related Issues or PRs
UN-4271. Parent epic UN-4257. Builds on UN-4270 [FEAT] v1 scaffold, strict TypeScript and import boundary #2380 (P0-T01) and UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 (P0-T04).
UN-4345 [FEAT] Frontend v1 E2E harness and the frontend-v1-e2e group #2355 (P0-T09 E2E harness): its
contract/registry.tscan now re-exportPAGES/DEMO_PAGES/REGISTERED_PAGES. It also needs:unstract.v1.oldVersion.<userId>with value"true";unstract.v1.lastUser, holding the user id: a page load that starts on a v1 URL takes the user from it, so the Human Review round trip must seed it or open a v0 page first;{ kind: "localStorage", key: "unstract.v1.preview" };DEMO-REVIEWexcluded from its "only UN11 toggles" self-test.All three keys can be imported without Vite:
PREVIEW_STORAGE_KEYfromregistry/types.ts, andoldVersionStorageKey,OLD_VERSION_ONandLAST_USER_STORAGE_KEYfromlib/oldVersion.ts.The E2E specs for the redirect and the Human Review round trip follow once it merges.
UN-4278 (P2-T10): the
resolveUrlinterface.Dependencies Versions
Notes on Testing
bun run typecheck:v1,bun run lint:v1,bun run test:v1: 115 v1 tests pass (1 deliberate skip) and all 89 boundary fixtures pass.bun run typecheck,biome ci src/and the fullvitest runpass: 769 tests.vite buildpasses withVITE_V1_DEV_ROUTESon and off, and fails withVITE_DISTRIBUTION=saas.replaceon the redirect, the release-build preview gate, distribution filtering, the v1 error boundary, param encoding, the redirect-loop check or the toggle check inresolveUrlfails a test.VersionSwitchredirects and the Human Review round trip.VITE_V1_DEV_ROUTES=truedev server, with no page errors:/acme/v0-demo?x=1lands on/acme/v1-demo?x=1with the v1 root;/acme/v1-demo-review/detailslands on/acme/v0-demo-reviewin the same document;/a%2Fb/v0-demolands on/a%2Fb/v1-demo.Screenshots
Checklist
I have read and understood the Contribution Guidelines.