Complete Python eval removal: epoch protocol, scope consolidation, JIT fixes
Route all rendering through OCaml bridge — render_to_html no longer uses Python async_eval. Fix register_components to parse &key params and &rest children from defcomp forms. Remove all dead sx_ref.py imports. Epoch protocol (prevents pipe desync): - Every command prefixed with (epoch N), all responses tagged with epoch - Both sides discard stale-epoch messages — desync structurally impossible - OCaml main loop discards stale io-responses between commands Consolidate scope primitives into sx_scope.ml: - Single source of truth for scope-push!/pop!/peek, collect!/collected, emit!/emitted, context, and 12 other scope operations - Removes duplicate registrations from sx_server.ml (including bugs where scope-emit! and clear-collected! were registered twice with different impls) - Bind scope prims into env so JIT VM finds them via OP_GLOBAL_GET JIT VM fixes: - Trampoline thunks before passing args to CALL_PRIM - as_list resolves thunks via _sx_trampoline_fn - len handles all value types (Bool, Number, RawHTML, SxExpr, Spread, etc.) Other fixes: - ~cssx/tw signature: (tokens) → (&key tokens) to match callers - Minimal Python evaluator in html.py for sync sx() Jinja function - Python scope primitive stubs (thread-local) for non-OCaml paths - Reader macro resolution via OcamlSync instead of sx_ref.py Tests: 1114 OCaml, 1078 JS, 35 Python regression, 6/6 Playwright SSR Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
558
shared/sx/tests/test_post_removal_bugs.py
Normal file
558
shared/sx/tests/test_post_removal_bugs.py
Normal file
@@ -0,0 +1,558 @@
|
||||
"""Tests exposing bugs after sx_ref.py removal.
|
||||
|
||||
These tests document all known breakages from removing the Python SX evaluator.
|
||||
Each test targets a specific codepath that was depending on sx_ref.py and is now
|
||||
broken.
|
||||
|
||||
Usage:
|
||||
pytest shared/sx/tests/test_post_removal_bugs.py -v
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||
if _project_root not in sys.path:
|
||||
sys.path.insert(0, _project_root)
|
||||
|
||||
from shared.sx.parser import parse, parse_all, serialize
|
||||
from shared.sx.types import Component, Symbol, Keyword, NIL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: load shared components fresh (no cache)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_components_fresh():
|
||||
"""Load shared components, clearing cache to force re-parse."""
|
||||
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
||||
_COMPONENT_ENV.clear()
|
||||
from shared.sx.components import load_shared_components
|
||||
load_shared_components()
|
||||
return _COMPONENT_ENV
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. register_components() loses all parameter information
|
||||
# ===========================================================================
|
||||
|
||||
class TestComponentRegistration(unittest.TestCase):
|
||||
"""register_components() hardcodes params=[] and has_children=False
|
||||
for every component, losing all parameter metadata."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.env = _load_components_fresh()
|
||||
|
||||
def test_shell_component_should_have_params(self):
|
||||
"""~shared:shell/sx-page-shell has 17+ &key params but gets params=[]."""
|
||||
comp = self.env.get("~shared:shell/sx-page-shell")
|
||||
self.assertIsNotNone(comp, "Shell component not found")
|
||||
self.assertIsInstance(comp, Component)
|
||||
# BUG: params is [] — should include title, meta-html, csrf, etc.
|
||||
self.assertGreater(
|
||||
len(comp.params), 0,
|
||||
f"Shell component has params={comp.params} — expected 17+ keyword params"
|
||||
)
|
||||
|
||||
def test_cssx_tw_should_have_tokens_param(self):
|
||||
"""~cssx/tw needs a 'tokens' parameter."""
|
||||
comp = self.env.get("~cssx/tw")
|
||||
self.assertIsNotNone(comp, "~cssx/tw component not found")
|
||||
self.assertIn(
|
||||
"tokens", comp.params,
|
||||
f"~cssx/tw has params={comp.params} — expected 'tokens'"
|
||||
)
|
||||
|
||||
def test_cart_mini_should_have_params(self):
|
||||
"""~shared:fragments/cart-mini has &key params."""
|
||||
comp = self.env.get("~shared:fragments/cart-mini")
|
||||
self.assertIsNotNone(comp, "cart-mini component not found")
|
||||
self.assertGreater(
|
||||
len(comp.params), 0,
|
||||
f"cart-mini has params={comp.params} — expected keyword params"
|
||||
)
|
||||
|
||||
def test_has_children_flag(self):
|
||||
"""Components with &rest children should have has_children=True."""
|
||||
comp = self.env.get("~shared:shell/sx-page-shell")
|
||||
self.assertIsNotNone(comp)
|
||||
# Many components accept children but has_children is always False
|
||||
# Check any component that is known to accept &rest children
|
||||
# e.g. a layout component
|
||||
for name, val in self.env.items():
|
||||
if isinstance(val, Component):
|
||||
# Every component has has_children=False — at least some should be True
|
||||
pass
|
||||
# Count how many have has_children=True
|
||||
with_children = sum(
|
||||
1 for v in self.env.values()
|
||||
if isinstance(v, Component) and v.has_children
|
||||
)
|
||||
total = sum(1 for v in self.env.values() if isinstance(v, Component))
|
||||
# BUG: with_children is 0 — at least some components accept children
|
||||
self.assertGreater(
|
||||
with_children, 0,
|
||||
f"0/{total} components have has_children=True — at least some should"
|
||||
)
|
||||
|
||||
def test_all_components_have_empty_params(self):
|
||||
"""Show the scale of the bug — every single component has params=[]."""
|
||||
components_with_params = []
|
||||
components_without = []
|
||||
for name, val in self.env.items():
|
||||
if isinstance(val, Component):
|
||||
if val.params:
|
||||
components_with_params.append(name)
|
||||
else:
|
||||
components_without.append(name)
|
||||
# BUG: ALL components have empty params
|
||||
self.assertGreater(
|
||||
len(components_with_params), 0,
|
||||
f"ALL {len(components_without)} components have params=[] — none have parameters parsed"
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. Sync html.py rendering is completely broken
|
||||
# ===========================================================================
|
||||
|
||||
class TestSyncHtmlRendering(unittest.TestCase):
|
||||
"""html.py render() stubs _raw_eval/_trampoline — any evaluation crashes."""
|
||||
|
||||
def test_html_render_simple_element(self):
|
||||
"""Even simple elements with keyword attrs need _eval, which is stubbed."""
|
||||
from shared.sx.html import render
|
||||
# This should work — (div "hello") needs no eval
|
||||
result = render(parse('(div "hello")'), {})
|
||||
self.assertIn("hello", result)
|
||||
|
||||
def test_html_render_with_keyword_attr(self):
|
||||
"""Keyword attrs go through _eval, which raises RuntimeError."""
|
||||
from shared.sx.html import render
|
||||
try:
|
||||
result = render(parse('(div :class "test" "hello")'), {})
|
||||
# If it works, great
|
||||
self.assertIn("test", result)
|
||||
except RuntimeError as e:
|
||||
self.assertIn("sx_ref.py has been removed", str(e))
|
||||
self.fail(f"html.py render crashes on keyword attrs: {e}")
|
||||
|
||||
def test_html_render_symbol_lookup(self):
|
||||
"""Symbol lookup goes through _eval, which is stubbed."""
|
||||
from shared.sx.html import render
|
||||
try:
|
||||
result = render(parse('(div title)'), {"title": "Hello"})
|
||||
self.assertIn("Hello", result)
|
||||
except RuntimeError as e:
|
||||
self.assertIn("sx_ref.py has been removed", str(e))
|
||||
self.fail(f"html.py render crashes on symbol lookup: {e}")
|
||||
|
||||
def test_html_render_component(self):
|
||||
"""Component rendering needs _eval for kwarg evaluation."""
|
||||
from shared.sx.html import render
|
||||
env = _load_components_fresh()
|
||||
try:
|
||||
result = render(
|
||||
parse('(~shared:fragments/cart-mini :cart-count 0 :blog-url "" :cart-url "")'),
|
||||
env,
|
||||
)
|
||||
self.assertIn("cart-mini", result)
|
||||
except RuntimeError as e:
|
||||
self.assertIn("sx_ref.py has been removed", str(e))
|
||||
self.fail(f"html.py render crashes on component calls: {e}")
|
||||
|
||||
def test_sx_jinja_function_broken(self):
|
||||
"""The sx() Jinja helper is broken — it uses html_render internally."""
|
||||
from shared.sx.jinja_bridge import sx
|
||||
env = _load_components_fresh()
|
||||
try:
|
||||
result = sx('(div "hello")')
|
||||
self.assertIn("hello", result)
|
||||
except RuntimeError as e:
|
||||
self.assertIn("sx_ref.py has been removed", str(e))
|
||||
self.fail(f"sx() Jinja function is broken: {e}")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. Async render_to_html uses Python path, not OCaml
|
||||
# ===========================================================================
|
||||
|
||||
class TestAsyncRenderToHtml(unittest.IsolatedAsyncioTestCase):
|
||||
"""helpers.py render_to_html() deliberately uses Python async_eval,
|
||||
not the OCaml bridge. But Python eval is now broken."""
|
||||
|
||||
async def test_render_to_html_uses_python_path(self):
|
||||
"""render_to_html goes through async_render, not OCaml bridge."""
|
||||
from shared.sx.helpers import render_to_html
|
||||
env = _load_components_fresh()
|
||||
# The shell component has many &key params — none are bound because params=[]
|
||||
try:
|
||||
html = await render_to_html(
|
||||
"shared:shell/sx-page-shell",
|
||||
title="Test", csrf="abc", asset_url="/static",
|
||||
sx_js_hash="abc123",
|
||||
)
|
||||
self.assertIn("Test", html)
|
||||
except Exception as e:
|
||||
# Expected: either RuntimeError from stubs or EvalError from undefined symbols
|
||||
self.fail(
|
||||
f"render_to_html (Python path) failed: {type(e).__name__}: {e}\n"
|
||||
f"This should go through OCaml bridge instead"
|
||||
)
|
||||
|
||||
async def test_async_render_component_no_params_bound(self):
|
||||
"""async_eval.py _arender_component can't bind params because comp.params=[]."""
|
||||
from shared.sx.async_eval import async_render
|
||||
from shared.sx.primitives_io import RequestContext
|
||||
env = _load_components_fresh()
|
||||
# Create a simple component manually with correct params
|
||||
test_comp = Component(
|
||||
name="test/greeting",
|
||||
params=["name"],
|
||||
has_children=False,
|
||||
body=parse('(div (str "Hello " name))'),
|
||||
)
|
||||
env["~test/greeting"] = test_comp
|
||||
try:
|
||||
result = await async_render(
|
||||
parse('(~test/greeting :name "World")'),
|
||||
env,
|
||||
RequestContext(),
|
||||
)
|
||||
self.assertIn("Hello World", result)
|
||||
except Exception as e:
|
||||
self.fail(
|
||||
f"async_render failed even with correct params: {type(e).__name__}: {e}"
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. Dead imports from removed sx_ref.py
|
||||
# ===========================================================================
|
||||
|
||||
class TestDeadImports(unittest.TestCase):
|
||||
"""Files that import from sx_ref.py will crash when their codepaths execute."""
|
||||
|
||||
def test_async_eval_defcomp(self):
|
||||
"""async_eval.py _asf_defcomp should work as a stub (no sx_ref import)."""
|
||||
from shared.sx.async_eval import _asf_defcomp
|
||||
env = {}
|
||||
asyncio.run(_asf_defcomp(
|
||||
[Symbol("defcomp"), Symbol("~test"), [], [Symbol("div")]],
|
||||
env, None
|
||||
))
|
||||
# Should register a minimal component in env
|
||||
self.assertIn("~test", env)
|
||||
|
||||
def test_async_eval_defmacro(self):
|
||||
"""async_eval.py _asf_defmacro should work as a stub (no sx_ref import)."""
|
||||
from shared.sx.async_eval import _asf_defmacro
|
||||
env = {}
|
||||
asyncio.run(_asf_defmacro(
|
||||
[Symbol("defmacro"), Symbol("test"), [], [Symbol("div")]],
|
||||
env, None
|
||||
))
|
||||
self.assertIn("test", env)
|
||||
|
||||
def test_async_eval_defstyle(self):
|
||||
"""async_eval.py _asf_defstyle should be a no-op (no sx_ref import)."""
|
||||
from shared.sx.async_eval import _asf_defstyle
|
||||
result = asyncio.run(_asf_defstyle(
|
||||
[Symbol("defstyle"), Symbol("test"), [], [Symbol("div")]],
|
||||
{}, None
|
||||
))
|
||||
# Should return NIL without crashing
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_async_eval_defhandler(self):
|
||||
"""async_eval.py _asf_defhandler should be a no-op (no sx_ref import)."""
|
||||
from shared.sx.async_eval import _asf_defhandler
|
||||
result = asyncio.run(_asf_defhandler(
|
||||
[Symbol("defhandler"), Symbol("test"), [], [Symbol("div")]],
|
||||
{}, None
|
||||
))
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_async_eval_continuation_reset(self):
|
||||
"""async_eval.py _asf_reset imports eval_expr/trampoline from sx_ref."""
|
||||
# The cont_fn inside _asf_reset will crash when invoked
|
||||
from shared.sx.async_eval import _ASYNC_RENDER_FORMS
|
||||
reset_fn = _ASYNC_RENDER_FORMS.get("reset")
|
||||
# reset is defined in async_eval — the import is deferred to execution
|
||||
# Just verify the module doesn't have the import available
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import eval_expr
|
||||
self.fail("sx_ref.py should not exist")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected
|
||||
|
||||
def test_ocaml_bridge_jit_compile(self):
|
||||
"""ocaml_bridge.py _compile_adapter_module imports from sx_ref."""
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import eval_expr, trampoline, PRIMITIVES
|
||||
self.fail("sx_ref.py should not exist — JIT compilation path is broken")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected — confirms the bug
|
||||
|
||||
def test_parser_reader_macro(self):
|
||||
"""parser.py _try_reader_macro imports trampoline/call_lambda from sx_ref."""
|
||||
try:
|
||||
from shared.sx.ref.sx_ref import trampoline, call_lambda
|
||||
self.fail("sx_ref.py should not exist — reader macros are broken")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected — confirms the bug
|
||||
|
||||
def test_primitives_scope_prims(self):
|
||||
"""primitives.py _lazy_scope_primitives silently fails to load scope prims."""
|
||||
from shared.sx.primitives import _PRIMITIVES
|
||||
# collect!, collected, clear-collected!, emitted, emit!, context
|
||||
# These are needed for CSSX but the import from sx_ref silently fails
|
||||
missing = []
|
||||
for name in ("collect!", "collected", "clear-collected!", "emitted", "emit!", "context"):
|
||||
if name not in _PRIMITIVES:
|
||||
missing.append(name)
|
||||
if missing:
|
||||
self.fail(
|
||||
f"Scope primitives missing from _PRIMITIVES (sx_ref import failed silently): {missing}\n"
|
||||
f"CSSX components depend on these for collect!/collected"
|
||||
)
|
||||
|
||||
def test_deps_transitive_deps_ref_path(self):
|
||||
"""deps.py transitive_deps imports from sx_ref when SX_USE_REF=1."""
|
||||
# The fallback path should still work
|
||||
from shared.sx.deps import transitive_deps
|
||||
env = _load_components_fresh()
|
||||
# Should work via fallback, not crash
|
||||
try:
|
||||
result = transitive_deps("~cssx/tw", env)
|
||||
self.assertIsInstance(result, set)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
self.fail(f"transitive_deps crashed: {e}")
|
||||
|
||||
def test_handlers_python_fallback(self):
|
||||
"""handlers.py eval_handler Python fallback imports async_eval_ref."""
|
||||
# When not using OCaml, handler evaluation falls through to async_eval
|
||||
# The ref path (SX_USE_REF=1) would crash
|
||||
try:
|
||||
from shared.sx.ref.async_eval_ref import async_eval_to_sx
|
||||
self.fail("async_eval_ref.py should not exist")
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pass # Expected
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 5. ~cssx/tw signature mismatch
|
||||
# ===========================================================================
|
||||
|
||||
class TestCssxTwSignature(unittest.TestCase):
|
||||
"""~cssx/tw changed from (&key tokens) to (tokens) positional,
|
||||
but callers use :tokens keyword syntax."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.env = _load_components_fresh()
|
||||
|
||||
def test_cssx_tw_source_uses_positional(self):
|
||||
"""Verify the current source has positional (tokens) not (&key tokens)."""
|
||||
import os
|
||||
cssx_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "templates", "cssx.sx"
|
||||
)
|
||||
with open(cssx_path) as f:
|
||||
source = f.read()
|
||||
# Check if it's positional or keyword
|
||||
if "(defcomp ~cssx/tw (tokens)" in source:
|
||||
# Positional — callers using :tokens will break
|
||||
self.fail(
|
||||
"~cssx/tw uses positional (tokens) but callers use :tokens keyword syntax.\n"
|
||||
"Should be: (defcomp ~cssx/tw (&key tokens) ...)"
|
||||
)
|
||||
elif "(defcomp ~cssx/tw (&key tokens)" in source:
|
||||
pass # Correct
|
||||
else:
|
||||
# Unknown signature
|
||||
for line in source.split("\n"):
|
||||
if "defcomp ~cssx/tw" in line:
|
||||
self.fail(f"Unexpected ~cssx/tw signature: {line.strip()}")
|
||||
|
||||
def test_cssx_tw_callers_use_keyword(self):
|
||||
"""Scan for callers that use :tokens keyword syntax."""
|
||||
import glob as glob_mod
|
||||
sx_dir = os.path.join(os.path.dirname(__file__), "../../..")
|
||||
keyword_callers = []
|
||||
positional_callers = []
|
||||
for fp in glob_mod.glob(os.path.join(sx_dir, "**/*.sx"), recursive=True):
|
||||
try:
|
||||
with open(fp) as f:
|
||||
content = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
if "~cssx/tw" not in content:
|
||||
continue
|
||||
for line_no, line in enumerate(content.split("\n"), 1):
|
||||
if "~cssx/tw" in line and "defcomp" not in line:
|
||||
if ":tokens" in line:
|
||||
keyword_callers.append(f"{fp}:{line_no}")
|
||||
elif "(~cssx/tw " in line:
|
||||
positional_callers.append(f"{fp}:{line_no}")
|
||||
|
||||
if keyword_callers:
|
||||
# If signature is positional but callers use :tokens, that's a bug
|
||||
import os as os_mod
|
||||
cssx_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "templates", "cssx.sx"
|
||||
)
|
||||
with open(cssx_path) as f:
|
||||
source = f.read()
|
||||
if "(defcomp ~cssx/tw (tokens)" in source:
|
||||
self.fail(
|
||||
f"~cssx/tw uses positional params but {len(keyword_callers)} callers use :tokens:\n"
|
||||
+ "\n".join(keyword_callers[:5])
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 6. OCaml bridge rendering (should work — this is the good path)
|
||||
# ===========================================================================
|
||||
|
||||
class TestOcamlBridgeRendering(unittest.IsolatedAsyncioTestCase):
|
||||
"""The OCaml bridge should handle all rendering correctly."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from shared.sx.ocaml_bridge import _DEFAULT_BIN
|
||||
bin_path = os.path.abspath(_DEFAULT_BIN)
|
||||
if not os.path.isfile(bin_path):
|
||||
raise unittest.SkipTest("OCaml binary not found")
|
||||
|
||||
async def asyncSetUp(self):
|
||||
from shared.sx.ocaml_bridge import OcamlBridge
|
||||
self.bridge = OcamlBridge()
|
||||
await self.bridge.start()
|
||||
|
||||
async def asyncTearDown(self):
|
||||
if hasattr(self, 'bridge'):
|
||||
await self.bridge.stop()
|
||||
|
||||
async def test_simple_element(self):
|
||||
result = await self.bridge.render('(div "hello")')
|
||||
self.assertIn("hello", result)
|
||||
|
||||
async def test_element_with_keyword_attrs(self):
|
||||
result = await self.bridge.render('(div :class "test" "hello")')
|
||||
self.assertIn('class="test"', result)
|
||||
self.assertIn("hello", result)
|
||||
|
||||
async def test_component_with_params(self):
|
||||
"""OCaml should handle component parameter binding correctly."""
|
||||
# Use load_source to define a component (bypasses _ensure_components lock)
|
||||
await self.bridge.load_source('(defcomp ~test/greet (&key name) (div (str "Hello " name)))')
|
||||
result = await self.bridge.render('(~test/greet :name "World")')
|
||||
self.assertIn("Hello World", result)
|
||||
|
||||
async def test_let_binding(self):
|
||||
result = await self.bridge.render('(let ((x "hello")) (div x))')
|
||||
self.assertIn("hello", result)
|
||||
|
||||
async def test_conditional(self):
|
||||
result = await self.bridge.render('(if true (div "yes") (div "no"))')
|
||||
self.assertIn("yes", result)
|
||||
self.assertNotIn("no", result)
|
||||
|
||||
async def test_cssx_tw_keyword_call(self):
|
||||
"""Test that ~cssx/tw works when called with :tokens keyword.
|
||||
Components are loaded by _ensure_components() automatically."""
|
||||
try:
|
||||
result = await self.bridge.render('(div (~cssx/tw :tokens "bg-red-500") "content")')
|
||||
# Should produce a spread with CSS class, not an error
|
||||
self.assertNotIn("error", result.lower())
|
||||
except Exception as e:
|
||||
self.fail(f"~cssx/tw :tokens keyword call failed: {e}")
|
||||
|
||||
async def test_cssx_tw_positional_call(self):
|
||||
"""Test that ~cssx/tw works when called positionally."""
|
||||
try:
|
||||
result = await self.bridge.render('(div (~cssx/tw "bg-red-500") "content")')
|
||||
self.assertNotIn("error", result.lower())
|
||||
except Exception as e:
|
||||
self.fail(f"~cssx/tw positional call failed: {e}")
|
||||
|
||||
async def test_repeated_renders_dont_crash(self):
|
||||
"""Verify OCaml bridge handles multiple sequential renders."""
|
||||
for i in range(5):
|
||||
result = await self.bridge.render(f'(div "iter-{i}")')
|
||||
self.assertIn(f"iter-{i}", result)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 7. Scope primitives missing (collect!, collected, etc.)
|
||||
# ===========================================================================
|
||||
|
||||
class TestScopePrimitives(unittest.TestCase):
|
||||
"""Scope primitives needed by CSSX are missing because the import
|
||||
from sx_ref.py silently fails."""
|
||||
|
||||
def test_python_primitives_have_scope_ops(self):
|
||||
"""Check that collect!/collected/etc. are in _PRIMITIVES."""
|
||||
from shared.sx.primitives import _PRIMITIVES
|
||||
required = ["collect!", "collected", "clear-collected!",
|
||||
"emitted", "emit!", "context"]
|
||||
missing = [p for p in required if p not in _PRIMITIVES]
|
||||
if missing:
|
||||
self.fail(
|
||||
f"Missing Python-side scope primitives: {missing}\n"
|
||||
f"These were provided by sx_ref.py — need OCaml bridge or Python stubs"
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 8. Query executor fallback path
|
||||
# ===========================================================================
|
||||
|
||||
class TestQueryExecutorFallback(unittest.TestCase):
|
||||
"""query_executor.py imports async_eval for its fallback path."""
|
||||
|
||||
def test_query_executor_import(self):
|
||||
"""query_executor can be imported without crashing."""
|
||||
try:
|
||||
import shared.sx.query_executor
|
||||
except Exception as e:
|
||||
self.fail(f"query_executor import crashed: {e}")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 9. End-to-end: sx_page shell rendering
|
||||
# ===========================================================================
|
||||
|
||||
class TestShellRendering(unittest.IsolatedAsyncioTestCase):
|
||||
"""The shell template needs to render through some path that works."""
|
||||
|
||||
async def test_sx_page_shell_via_python(self):
|
||||
"""render_to_html('shared:shell/sx-page-shell', ...) uses Python path.
|
||||
This is the actual failure from the production error log."""
|
||||
from shared.sx.helpers import render_to_html
|
||||
_load_components_fresh()
|
||||
try:
|
||||
html = await render_to_html(
|
||||
"shared:shell/sx-page-shell",
|
||||
title="Test Page",
|
||||
csrf="test-csrf",
|
||||
asset_url="/static",
|
||||
sx_js_hash="abc",
|
||||
)
|
||||
# Should produce full HTML document
|
||||
self.assertIn("<!doctype html>", html.lower())
|
||||
self.assertIn("Test Page", html)
|
||||
except Exception as e:
|
||||
self.fail(
|
||||
f"Shell rendering via Python path failed: {type(e).__name__}: {e}\n"
|
||||
f"This is the exact error seen in production — "
|
||||
f"render_to_html should use OCaml bridge"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user