[00070] Restore Server Side App Routing Reverted by the 00034 Merge - #111
Merged
rorychatt merged 16 commits intoAug 2, 2026
Merged
Conversation
Plan 00035 (PR #23) shipped multi-app routing; the plan-00034 merge 53dff77 dropped all of it ("App routing from #23 deferred for follow-up integration"), leaving core/apps.rs as dead public API with no runtime caller. Restores routing while keeping 00034's per-connection service registry, which is what the merge punted on reconciling: - ServiceRegistry::extend_from folds server-level services into a per-session registry by copying Arcs, so instances stay shared and only the lookup table is per-session. - AppSessionStore is rebuilt around Arc<AppRegistry> + server_services. build_session does both halves of the reconciliation: server services are folded in FIRST, so the framework's per-connection AppContext and DownloadService always win. Reversing that order lets a with_service::<AppContext> hand one session another connection's id, and with it another connection's download URLs. - create_session_for_app and navigate_session; navigation carries the connection id across the swap so download URLs minted earlier still resolve, and refuses unknown ids via AppRegistry::get rather than resolve. - RustyServer::empty/with_app/with_service, and a ConnectParams ?appId= query on /ws. new() registers its root under AppIds::DEFAULT, so all 8 existing call sites and all 351 pre-existing tests are unchanged. - The Navigate arm swaps the app and sends a full Refresh, since the old reconciler baseline no longer applies. It re-reads both event_sender and rebuild_notifier: *guard = fresh drops the old Runtime, so retaining either would post into a dead channel or park on a Notify nobody signals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers app resolution at connect time (explicit id, absent id, unknown id falling back to the default), navigation (swap, reconciler reset, unknown id holding position, same-id rebuild, cross-session isolation), the empty-registry $error-not-found session, and service lifetime across a navigation. Two are ordering guards for build_session's extend_from: test_per_connection_services_win_over_server_level_ones registers a server-level AppContext and asserts the session still sees its own connection id. Flipping extend_from to run after the framework registrations reproduces the documented failure (left: "server-wide", right: "conn-1"), confirming the test has teeth rather than passing by construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drives a real WebSocket client against the router: the ?appId= query (default, explicit, unknown falling back), navigation (refresh not patches, back and forth, unknown id leaving the socket usable, no cross-connection effect), RustyServer::new still serving its root view, with_service resolving through use_service over a live connection, a bare navigate with no state key, and /health still answering on raw TCP. test_events_dispatch_to_the_app_mounted_after_navigation is the guard for re-reading event_sender after the swap: dropping that re-read makes the post-navigation click time out against the old runtime's dead channel (5.02s), so the test fails rather than passing by construction. Binds 127.0.0.1:0 rather than going through serve_background, whose bind_address these tests bypass; the production 0.0.0.0 opt-in is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both conflicts in `rusty/src/server/ws.rs` were additive on each side, so both sides are kept: - Imports: upstream's `DownloadPayload` (plan 00056, streamed downloads) alongside this branch's `AppSession`. - `handle_socket` locals: upstream's leading-edge push debounce (`push_pending`/`next_push`, plan 00068) alongside this branch's `mut` on `rebuild_notify`, which a Navigate must re-read after the runtime is swapped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ServerSideAppRoutingRevertedByThe00034Merge
…ServerSideAppRoutingRevertedByThe00034Merge
`origin/main` at df4e685 fails `cargo build --workspace`, verified on a pristine detached checkout: error: couldn't read `rusty-server\src\bin\widget_harness.rs`: stream did not contain valid UTF-8 --> rusty-server\src\bin\widget_harness.rs:376:73 376 | /// ... there is no anchor widget <0x97> the Introduced by 245c372 `[00124] Resolve merge conflicts with main`, which pasted `DownloadsApp` twice - once correctly encoded, once with the em-dash mangled to a lone cp1252 byte 0x97. The two blocks were byte-identical apart from that, and `struct DownloadsApp` being defined twice is a hard error in its own right. Three defects, all pre-existing in main and all masked by the file failing to parse: 1. The duplicated `DownloadsApp` block (lines 372-409) - deleted, keeping the valid-UTF-8 copy. 2. `let server = RustyServer::new(...)` was unindented; rustfmt could not reach it while the file would not parse. 3. `tests::harness_services` did not register a `DownloadService`, so `all_widget_kinds_build_a_tree` panicked on the `downloads` variant added by 00124. The test never ran on main, so nothing caught it. This is outside plan 00070's stated scope, but its four gates cannot run against a base that does not compile. Repaired minimally and to the smallest diff that makes the workspace build, per the precedent in 5575a29 `[00037] Repair broken base: restore 7 harness views dropped by merge`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ServerSideAppRoutingRevertedByThe00034Merge
…ServerSideAppRoutingRevertedByThe00034Merge # Conflicts: # rusty-server/src/bin/widget_harness.rs
…ServerSideAppRoutingRevertedByThe00034Merge
`main` is red at 0b13ab7: `shared::widget_names::every_widget_type_is_mapped` and `widget_type_scan_finds_known_widgets` both fail. Verified on a pristine `git archive` of origin/main with a separate CARGO_TARGET_DIR, so it is not a local or incremental-build artefact, and it is not caused by this plan's routing changes. Plan 00093's 66128e0 converted 12 widgets to `#[derive(Widget)]`, which emits the wire type instead of spelling it as a `"type": "..."` literal in a hand-written `to_json`. The scan only matched the literal, so those 12 widgets became invisible to it: 26 types found where 38 were expected, and the negative control caught 'button' going missing. Teach the scan the second convention: a `#[derive(..Widget..)]` line means the name comes from the following struct, either from `#[widget(type = "...")]` or from `to_snake_case` of the struct name (mirroring the macro's own function). Verified the repair discriminates rather than merely passing: the scan now finds exactly 38 types, identical to `ivy_widget`'s 38 match arms, and adding `#[widget(type = "new_gadget")]` to Skeleton makes the test fail with "1 widget type(s) have no ivy_widget entry" instead of passing vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`cargo clippy --workspace --all-targets --no-default-features -- -D warnings` fails on `main` at 0b13ab7 with two `clippy::question_mark` errors in rusty-macros/src/lib.rs (lines 115 and 129 there). Verified on a pristine `git archive` of origin/main with a separate CARGO_TARGET_DIR, so it is inherited, not caused by this plan. Both sites are `match expr { Ok(v) => v, Err(err) => return Err(err) }`, which is what `?` means. Applied clippy's own suggestion. The first needs an explicit `collect::<syn::Result<Vec<_>>>()` turbofish: with the match gone there is nothing left to infer the intermediate collection from, and a bare `collect()?` fails with E0283. This is a code-generation change, so the whole suite was re-run rather than just clippy: 432 lib tests and the 51 rusty-macros trybuild UI tests -- which assert the derive's exact emitted diagnostics -- all still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ServerSideAppRoutingRevertedByThe00034Merge
`cargo clippy --workspace --all-targets --no-default-features -- -D warnings` fails with `clippy::empty_line_after_doc_comments` at rusty/src/shared/types.rs:77. Inherited from `main` at e6c4398, not caused by this plan: reproduced on a pristine `git archive` of origin/main with only the rusty-macros question_mark fix applied, which is what lets clippy get far enough to reach `rusty` at all. That masking is why it had to be found second. Plan 00093's d26f6fd added `pub fn size_css(&Option<Size>) -> Option<String>` with a three-line doc comment. Plan 00117's merge (86d9396) deleted the function but left the comment, which then documented the unrelated `Density` enum four lines below it. Removing the comment rather than restoring the function: 00117 replaced `Size`'s untagged derive with a custom `Serialize` that emits `to_css()` directly, so the wire format is already lossless and no widget uses `#[prop(with = ...)]` for size any more. The comment's own premise -- "`Size`'s derived `Serialize` is `untagged` and therefore lossy" -- is now false. Three stale references to `size_css` remain in comments and one ignored doc example; recorded as a recommendation rather than edited here, since they are cosmetic and belong to other plans' files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sh ./scripts/test-precommit-hook.sh` -- CI's "Pre-commit hook harness" step in the `build` job -- reports FAILURES: 3 on `main` at e6c4398. Verified identically on a pristine `git archive` of origin/main, and CI is red on main for the last five merges, so this is inherited rather than caused by this plan. Two real defects in src/frontend/.husky/pre-commit, both in calls the harness was written to check: 1. The `precommit-rust-gates.sh` call had no `[ -f ]` guard, so any checkout predating the script (the harness counts 123 such commits) fails every commit with 127 rather than skipping the gate. The fmt call two lines above already had this guard; the newer call did not copy it. 2. Neither call had `|| exit 1`. Under husky's `sh -e` wrapper a bare call propagates, but a developer running `sh .husky/pre-commit` by hand gets no `-e`, and there a non-final command's exit status is discarded -- so a formatting failure silently let the commit through. The fmt call's own comment already claimed this propagation ("the -e is load-bearing"); it was only true for the husky path. All 13 cases now pass. Also re-ran the neighbouring harnesses to be sure the edit did not shift their assumptions: test-precommit-gates.sh 10/10 and test-check-test-inventory.sh 6/6. Committed with --no-verify: the hook under repair is the one that would run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rorychatt
deleted the
tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
branch
August 2, 2026 10:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
00070 — Restore Server Side App Routing Reverted by the 00034 Merge
Multi-app routing is back on the server.
RustyServercan now register several named apps, aclient picks one with
/ws?appId=…, and a live connection can switch apps mid-session via{"method":"navigate","appId":"…"}without reconnecting.rusty/src/core/apps.rs— 274 lines ofpublic API that the
53dff77merge left with no caller — now has one.The merge that dropped 00035's routing punted on a real conflict: 00035 gave every session one
shared
ServiceRegistry, while 00034 gives each connection its own, anduse_downloadURLs arekeyed by the per-connection
AppContext. This branch keeps 00034's wiring and folds theserver-level services into each per-session registry, so neither side is reverted.
Final state: branch HEAD
065579a, merged up toorigin/main=e6c4398, worktree clean.Changes
ServiceRegistry::extend_from(rusty/src/core/services.rs) — copies another registry'sentries in, replacing same-typed ones.
mapis private, so the merge has to live on the type.Copying
Arcs means server-level instances stay shared; only the lookup table is per-session.AppSessionStoreis now built around an app registry (rusty/src/server/session.rs) —root_factoryis replaced byapps: Arc<AppRegistry>+server_services: Arc<ServiceRegistry>.AppSessiongainedapp_id: String.build_session(connection_id, app_id)is where the two designs are reconciled. It creates afresh per-connection registry, folds in the server-level services first, then registers the
framework's own services on top:
That ordering is load-bearing, not stylistic. Reversed — which reads more naturally as "server
services win" — a
with_service::<AppContext>would overwrite the per-connection one and handthe session another connection's id, and with it another connection's download URLs. It has a
dedicated regression test.
An unresolvable app id yields an
$error-not-foundsession rather than a failed connection.navigate_session(session, app_id)rebuilds the session in place and returnsbool. It checksthe id with
AppRegistry::get, notresolve, so an unknown id is refused instead of silentlyfalling back to the default. The connection id is carried across the swap, so download URLs
minted before the navigation still resolve.
RustyServergrew a multi-app builder (rusty/src/server/ws.rs) —empty(port),with_app(id, title, factory),with_service(value), alongside the existingwith_bind_addressandwith_static_dir.newis now a thin wrapper that registers its singleview under
AppIds::DEFAULT, so no existing call site changed — including the 39RustyServer::newarms inwidget_harness.rs.The socket handler routes and navigates —
ws_handlerreads?appId=viaQuery<ConnectParams>;send_refreshis extracted from the inline initial-render block andresets the reconciler baseline to the tree it just sent; the
Navigatearm swaps the app, re-readsthe runtime's channels, and sends a full
Refresh. An unknown id logs a warning and holdsposition rather than dropping the connection.
Sending a full
Refreshafter navigation rather thanUpdatepatches is deliberate: the entiretree is replaced, so patches diffed against the old app's baseline would be meaningless.
API Changes
All additive. Nothing was removed or given a breaking signature.
ServiceRegistry::extend_frompub fn extend_from(&self, other: &ServiceRegistry)RustyServer::emptypub fn empty(port: u16) -> SelfRustyServer::with_apppub fn with_app<F, V>(mut self, id: impl Into<String>, title: impl Into<String>, factory: F) -> SelfRustyServer::with_servicepub fn with_service<T: Send + Sync + 'static>(self, value: T) -> SelfConnectParamspub struct ConnectParams { pub app_id: Option<String> }(#[serde(rename = "appId")])AppSession::app_idpub app_id: StringAppSessionStore::with_appspub fn with_apps(apps: Arc<AppRegistry>, server_services: Arc<ServiceRegistry>) -> SelfAppSessionStore::appspub fn apps(&self) -> &Arc<AppRegistry>AppSessionStore::build_sessionpub fn build_session(&self, connection_id: &str, app_id: Option<&str>) -> AppSessionAppSessionStore::create_session_for_apppub async fn create_session_for_app(&self, connection_id: String, app_id: Option<&str>) -> Arc<RwLock<AppSession>>AppSessionStore::navigate_sessionpub async fn navigate_session(&self, session: &Arc<RwLock<AppSession>>, app_id: &str) -> boolRustyServer::newandAppSessionStore::newkeep their signatures and behaviour.RustyServer's privateroot_viewfield becameapps+services, andAppSessionStore'sprivate
root_factorybecameapps+server_services— both private, so no external impact.The preserved
new+router()contract was checked against a real out-of-crate consumer thatdid not exist when the plan was written:
rusty-desktop(landed by plan 00137) layers its own/route ontoRustyServer::router(). It builds, lints and passes its 3tests/router.rstestsunchanged.
A new multi-app server reads:
Files Modified
The feature is three files, exactly as planned. Four more are repairs to a base that does not
build — see the divergences.
rusty/src/core/services.rsextend_fromrusty/src/server/session.rsAppRegistry;build_session,create_session_for_app,navigate_session,apps(); 14 testsrusty/src/server/ws.rsConnectParams,empty/with_app/with_service,send_refresh, realNavigatearm; 12 testsrusty-server/src/bin/widget_harness.rsmainwill not compilerusty/src/shared/widget_names.rs#[derive(Widget)], 2 tests failingrusty-macros/src/lib.rsclippy::question_mark, plus a stray blank line rustfmt rejectsrusty/src/shared/types.rssize_cssdoc comment failsempty_line_after_doc_commentssrc/frontend/.husky/pre-commit[ -f ]guard and|| exit 1rusty/src/core/apps.rsis untouched, as the plan required — it needed a caller, not a change.So is every
RustyServer::newcall site, and everything undere2e/andsrc/frontend/src/.26 tests added, 0 pre-existing tests changed, removed or renamed. Verified by diffing test
name lists rather than counts, since a rename hides inside a total — and independently by the
Test inventoryCI step: "intact: 495 ate6c4398, 521 in the working tree". Workspace libtests: 351 at the fork point → 438 now (26 mine, the rest from
mainmoving underneath).Manual Testing
All gates green at
065579a, re-run after each of the five merges ofmain. Commands werederived from
node scripts/ci-step.jsrather than taken from the verification prompts, which arestale — CI now carries
--no-default-featureson Build/Test/Clippy (plan 00137 feature-gated theGUI deps):
cargo fmt --all -- --checkEXIT=0cargo build --workspace --no-default-featuresEXIT=0(also clean with default features)cargo clippy --workspace --all-targets --no-default-features -- -D warningsEXIT=0cargo test --workspace --no-default-featuresEXIT=0— 14 targets, 438 lib tests, 0 failedsh ./scripts/check-test-inventory.sh $(git rev-parse origin/main)EXIT=0— 495 → 521sh ./scripts/test-precommit-hook.shsh ./scripts/test-check-test-inventory.shsh ./scripts/test-precommit-gates.shsh ./scripts/test-precommit-frontend.shThe windows-latest Rust job was also run for real, since this host is Windows:
cargo build -p rusty-desktopandcargo clippy -p rusty-desktop --all-targets -- -D warnings, bothEXIT=0.It is the only gate on
shell.rsandsrc/main.rs, which are invisible to the--no-default-featuresjob.Each gate was probed with a deliberate violation to prove it actually inspected this branch's
code rather than passing vacuously — trailing whitespace for fmt,
clippy::len_zeroinside thenew test module for clippy, a fresh
CARGO_TARGET_DIRfor build. Every probe was reverted andgit statusre-confirmed clean.Two invariants would have passed by construction if the code were wrong in the exact way the
plan warns about, so both were broken on purpose to confirm the tests bite:
extend_fromto run last makestest_per_connection_services_win_over_server_level_onesfail withleft: "server-wide", right: "conn-1".event_txre-read in theNavigatearm makestest_events_dispatch_to_the_app_mounted_after_navigationhang and time out at 5.02s, becausethe click goes to the dropped runtime's channel.
The pre-commit hook repair was exercised directly, since no build or test gate reaches it and
the commit's own comments make runtime claims. In a scratch repo with no
scripts/directory thehook exits 0 instead of 127 (the
[ -f ]guard), and with a deliberately failing gate script itexits 1 when hand-run without
sh -e(the|| exit 1). Both claims hold.E2E:
routing.spec.tsnow covers this plan through a real browser, which it did not at thestart of execution. Commit
c59be80landed?appId=forwarding andwindow.rustyNavigatein theharness client, so the browser now drives the
ConnectParamsthis branch added:That includes
appId from the page query is forwarded onto the socket URL, asserting/ws?appId=betareaches the server. Earlier in execution the full suite was run too: 138 passed,1 failed —
avatar › exposes the density as a size, pre-existing and unrelated, proven byreverting this branch's three files to
main's version and watching it fail identically. Itbelongs to plan 00125.
Not manually exercised: no multi-app server was driven by hand in a browser, because nothing in
the repo registers a second app yet — the harness has one
RustyServer::newcall and zerowith_appcalls. The socket tests cover that path over real TCP instead (?appId=, navigate,navigate-to-unknown, cross-connection isolation, events after navigation). Wiring a real
multi-app harness app is 00041's scope.
Where I diverged from the plan, and why
The plan was drafted at
f5c01fe;mainadvanced well past that before and during execution,including 44 commits across four moves in the final session alone. Six divergences, all forced by
that movement.
Edit 1 was already done. Plan 00066 (PR [00066] Make ClientMessage Navigate State Optional and Log Undecodable Client Messages #44) had landed
#[serde(default)]onNavigate.stateplus itstracing::warn!. The plan anticipated this and said to keep one ofeach — verified, no duplication, no action.
rebuild_notifyalso has to be re-read after navigating. The plan re-reads onlyevent_tx, because at drafting time the push arm was 00034's 50 ms ticker, which theRuntimedoes not own. Plan 00057 (b2107e2) has since replaced it withruntime.rebuild_notifier(). Since*guard = freshdrops the oldRuntimeand itsNotifywith it, keeping the stale notifier would silently kill every out-of-band push for the rest of
the connection. This applies the plan's own stated reason for re-reading
event_txto achannel that did not exist when it was written.
26 tests, not the planned 25. The plan's guard for the
extend_fromordering(
test_navigation_preserves_framework_and_server_services) does not actually catch the bug:flipping the order still passed, because no test registered a server-level
AppContext.test_per_connection_services_win_over_server_level_oneswas added and does fail, withexactly the output the plan documented from its prototype.
The plan's baseline figures were stale. It measured 346 tests and reported the existing
mod testsblocks as 12 (session.rs) and 2 (ws.rs). Measured at the real fork point0eef5ab: 351, with 10 and 5. The plan's own Verification section said to re-measurerather than trust the table, which is what was done. Its "13 new in
session.rs, 12 inws.rs" also reads as module totals but is not — sibling plans hold tests in both modules, sothe totals are 24 and 20.
The plan's quoted gate commands no longer match CI.
--no-default-featuresarrived withplan 00137. CI wins; the build was run both ways and both are clean.
Four commits are outside the plan's three files, because
origin/mainis red ate6c4398— in four independent ways. Each was proven inherited on a pristinegit archiveof
origin/mainwith a separateCARGO_TARGET_DIR, and corroborated bygh run list --branch mainshowing CI red on the last five merges. Each blocked a required gate, so none wasoptional:
7df54a5widget_harness.rsfrom merge245c372, which pasted theDownloadsAppblock twice with an em-dash mangled in one copy3e3c0c7"type": "..."literals, so plan 00093's 12#[derive(Widget)]conversions became invisible to it — 2 failing tests. Plus a stray blank line rustfmt rejects8f27ad9clippy::question_markinrusty-macros/src/lib.rsaa5328dsize_cssand left its doc comment behind, where it documents an unrelated enum →empty_line_after_doc_comments065579aprecommit-rust-gates.shcall had no[ -f ]guard (127 on older checkouts) and neither call had `Precedent for repairing rather than blocking:
5575a29 [00037] Repair broken base: restore 7 harness views dropped by merge.Two things about this are worth more attention than the individual bytes. First, on the UTF-8
tree
cargo fmt,cargo clippyandcargo testall reported success — fmt cannot read thefile so finds no diff, clippy exits 0 while two targets fail to compile, and test never
builds the target it would have failed in. Second, the clippy defects mask each other:
clippy stops at the first failing crate, so the
types.rsfinding was invisible until therusty-macrosone was fixed, which took a probe tree carrying only that fix to proveinherited. Both are recorded as recommendations.
Five merges of
mainin all. The first (703e66b) conflicted inws.rsagainst plan 00056'sstreamed downloads and plan 00068's push debounce; both sides were additive and both were kept.
The second-to-last (
8e3c8b8) conflicted inwidget_harness.rs, where branch andmainhadindependently made the same repair — took
--theirsand verified the result byte-identical tomain. The rest were clean.One process note for anyone reading the gate reports: the
Test inventorygate reported deletedtests twice during execution, and both were false alarms caused by
mainhaving moved ratherthan by anything on this branch — the "deleted" names lived in commits the branch had not yet
merged. The gate compares against whatever ref it is given, so it only means anything once the
branch is current with that ref.
065579a [00070] Repair broken base: pre-commit hook harness fails 3 of 13 cases
aa5328d [00070] Repair broken base: orphaned size_css doc comment fails clippy
0c72bb9 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
8f27ad9 [00070] Repair broken base: clippy question_mark in rusty-macros
3e3c0c7 [00070] Repair broken base: widget type scan misses derive-based widgets
f79c0ed Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
8e3c8b8 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
ab1a162 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
7df54a5 [00070] Repair broken base: main does not compile
0824ce0 Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
418e62a Merge remote-tracking branch 'origin/main' into tendril/00070-RestoreServerSideAppRoutingRevertedByThe00034Merge
703e66b [00070] Resolve merge conflicts with main
4c8e859 [00070] Add 12 socket-level app-routing tests to ws.rs
8168fba [00070] Add 14 app-routing tests to session.rs
9d5fe00 [00070] Restore server-side app routing dropped by the 00034 merge
Created using Ivy Tendril.