Files
rose-ash/shared/sx/tests/test_router.py
giles cf5e767510 Phase 3: Client-side routing with SX page registry + routing analyzer demo
Add client-side route matching so pure pages (no IO deps) can render
instantly without a server roundtrip. Page metadata serialized as SX
dict literals (not JSON) in <script type="text/sx-pages"> blocks.

- New router.sx spec: route pattern parsing and matching (6 pure functions)
- boot.sx: process page registry using SX parser at startup
- orchestration.sx: intercept boost links for client routing with
  try-first/fallback — client attempts local eval, falls back to server
- helpers.py: _build_pages_sx() serializes defpage metadata as SX
- Routing analyzer demo page showing per-page client/server classification
- 32 tests for Phase 2 IO detection (scan_io_refs, transitive_io_refs,
  compute_all_io_refs, component_pure?) + fallback/ref parity
- 37 tests for Phase 3 router functions + page registry serialization
- Fix bootstrap_py.py _emit_let cell variable initialization bug
- Fix missing primitive aliases (split, length, merge) in bootstrap_py.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:47:56 +00:00

301 lines
10 KiB
Python

"""Tests for the router.sx spec — client-side route matching.
Tests the bootstrapped Python router functions (from sx_ref.py) and
the SX page registry serialization (from helpers.py).
"""
import pytest
from shared.sx.ref import sx_ref
# ---------------------------------------------------------------------------
# split-path-segments
# ---------------------------------------------------------------------------
class TestSplitPathSegments:
def test_simple(self):
assert sx_ref.split_path_segments("/docs/hello") == ["docs", "hello"]
def test_root(self):
assert sx_ref.split_path_segments("/") == []
def test_trailing_slash(self):
assert sx_ref.split_path_segments("/docs/") == ["docs"]
def test_no_leading_slash(self):
assert sx_ref.split_path_segments("docs/hello") == ["docs", "hello"]
def test_single_segment(self):
assert sx_ref.split_path_segments("/about") == ["about"]
def test_deep_path(self):
assert sx_ref.split_path_segments("/a/b/c/d") == ["a", "b", "c", "d"]
def test_empty(self):
assert sx_ref.split_path_segments("") == []
# ---------------------------------------------------------------------------
# parse-route-pattern
# ---------------------------------------------------------------------------
class TestParseRoutePattern:
def test_literal_only(self):
result = sx_ref.parse_route_pattern("/docs/")
assert len(result) == 1
assert result[0]["type"] == "literal"
assert result[0]["value"] == "docs"
def test_param(self):
result = sx_ref.parse_route_pattern("/docs/<slug>")
assert len(result) == 2
assert result[0] == {"type": "literal", "value": "docs"}
assert result[1] == {"type": "param", "value": "slug"}
def test_multiple_params(self):
result = sx_ref.parse_route_pattern("/users/<uid>/posts/<pid>")
assert len(result) == 4
assert result[0]["type"] == "literal"
assert result[1] == {"type": "param", "value": "uid"}
assert result[2]["type"] == "literal"
assert result[3] == {"type": "param", "value": "pid"}
def test_root_pattern(self):
result = sx_ref.parse_route_pattern("/")
assert result == []
# ---------------------------------------------------------------------------
# match-route
# ---------------------------------------------------------------------------
class TestMatchRoute:
def test_exact_match(self):
params = sx_ref.match_route("/docs/", "/docs/")
assert params is not None
assert params == {}
def test_param_match(self):
params = sx_ref.match_route("/docs/components", "/docs/<slug>")
assert params is not None
assert params == {"slug": "components"}
def test_no_match_different_length(self):
result = sx_ref.match_route("/docs/a/b", "/docs/<slug>")
assert result is sx_ref.NIL or result is None
def test_no_match_literal_mismatch(self):
result = sx_ref.match_route("/api/hello", "/docs/<slug>")
assert result is sx_ref.NIL or result is None
def test_root_match(self):
params = sx_ref.match_route("/", "/")
assert params is not None
assert params == {}
def test_multiple_params(self):
params = sx_ref.match_route("/users/42/posts/7", "/users/<uid>/posts/<pid>")
assert params is not None
assert params == {"uid": "42", "pid": "7"}
# ---------------------------------------------------------------------------
# find-matching-route
# ---------------------------------------------------------------------------
class TestFindMatchingRoute:
def _make_routes(self, patterns):
"""Build route entries like boot.sx does — with parsed patterns."""
routes = []
for name, pattern in patterns:
route = {
"name": name,
"path": pattern,
"parsed": sx_ref.parse_route_pattern(pattern),
"has-data": False,
"content": "(div \"test\")",
}
routes.append(route)
return routes
def test_first_match(self):
routes = self._make_routes([
("home", "/"),
("docs-index", "/docs/"),
("docs-page", "/docs/<slug>"),
])
match = sx_ref.find_matching_route("/docs/components", routes)
assert match is not None
assert match["name"] == "docs-page"
assert match["params"] == {"slug": "components"}
def test_exact_before_param(self):
routes = self._make_routes([
("docs-index", "/docs/"),
("docs-page", "/docs/<slug>"),
])
match = sx_ref.find_matching_route("/docs/", routes)
assert match is not None
assert match["name"] == "docs-index"
def test_no_match(self):
routes = self._make_routes([
("home", "/"),
("docs-page", "/docs/<slug>"),
])
result = sx_ref.find_matching_route("/unknown/path", routes)
assert result is sx_ref.NIL or result is None
def test_root_match(self):
routes = self._make_routes([
("home", "/"),
("about", "/about"),
])
match = sx_ref.find_matching_route("/", routes)
assert match is not None
assert match["name"] == "home"
def test_params_not_on_original(self):
"""find-matching-route should not mutate the original route entry."""
routes = self._make_routes([("page", "/docs/<slug>")])
match = sx_ref.find_matching_route("/docs/test", routes)
assert match["params"] == {"slug": "test"}
# Original should not have params key
assert "params" not in routes[0]
# ---------------------------------------------------------------------------
# Page registry SX serialization
# ---------------------------------------------------------------------------
class TestBuildPagesSx:
"""Test the SX page registry format — serialize + parse round-trip."""
def test_round_trip_simple(self):
"""SX dict literal round-trips through serialize → parse."""
from shared.sx.helpers import _sx_literal
from shared.sx.parser import parse_all
# Build an SX dict literal like _build_pages_sx does
entry = (
"{:name " + _sx_literal("home")
+ " :path " + _sx_literal("/")
+ " :auth " + _sx_literal("public")
+ " :has-data false"
+ " :content " + _sx_literal("(~home-content)")
+ " :closure {}}"
)
parsed = parse_all(entry)
assert len(parsed) == 1
d = parsed[0]
assert d["name"] == "home"
assert d["path"] == "/"
assert d["auth"] == "public"
assert d["has-data"] is False
assert d["content"] == "(~home-content)"
assert d["closure"] == {}
def test_round_trip_multiple(self):
"""Multiple SX dict literals parse as a list."""
from shared.sx.helpers import _sx_literal
from shared.sx.parser import parse_all
entries = []
for name, path in [("home", "/"), ("docs", "/docs/<slug>")]:
entry = (
"{:name " + _sx_literal(name)
+ " :path " + _sx_literal(path)
+ " :has-data false"
+ " :content " + _sx_literal("(div)")
+ " :closure {}}"
)
entries.append(entry)
text = "\n".join(entries)
parsed = parse_all(text)
assert len(parsed) == 2
assert parsed[0]["name"] == "home"
assert parsed[1]["name"] == "docs"
assert parsed[1]["path"] == "/docs/<slug>"
def test_content_with_quotes(self):
"""Content expressions with quotes survive serialization."""
from shared.sx.helpers import _sx_literal
from shared.sx.parser import parse_all
content = '(~doc-page :title "Hello \\"World\\"")'
entry = (
"{:name " + _sx_literal("test")
+ " :content " + _sx_literal(content)
+ " :closure {}}"
)
parsed = parse_all(entry)
assert parsed[0]["content"] == content
def test_closure_with_values(self):
"""Closure dict with various value types."""
from shared.sx.helpers import _sx_literal
from shared.sx.parser import parse_all
entry = '{:name "test" :closure {:label "hello" :count 42 :active true}}'
parsed = parse_all(entry)
closure = parsed[0]["closure"]
assert closure["label"] == "hello"
assert closure["count"] == 42
assert closure["active"] is True
def test_has_data_true(self):
"""has-data true marks server-only pages."""
from shared.sx.parser import parse_all
entry = '{:name "analyzer" :path "/data" :has-data true :content "" :closure {}}'
parsed = parse_all(entry)
assert parsed[0]["has-data"] is True
# ---------------------------------------------------------------------------
# _sx_literal helper
# ---------------------------------------------------------------------------
class TestSxLiteral:
def test_string(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal("hello") == '"hello"'
def test_string_with_quotes(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal('say "hi"') == '"say \\"hi\\""'
def test_string_with_newline(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal("line1\nline2") == '"line1\\nline2"'
def test_string_with_backslash(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal("a\\b") == '"a\\\\b"'
def test_int(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal(42) == "42"
def test_float(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal(3.14) == "3.14"
def test_bool_true(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal(True) == "true"
def test_bool_false(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal(False) == "false"
def test_none(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal(None) == "nil"
def test_empty_string(self):
from shared.sx.helpers import _sx_literal
assert _sx_literal("") == '""'