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>
301 lines
10 KiB
Python
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("") == '""'
|