The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
"""
|
|
SSR behavior — the genuine capability behind the `ssr_bridge` probe.
|
|
|
|
The SSR subprocess lifecycle + JSON-RPC protocol live in the shared
|
|
`mizan_core.ssr.SSRBridge`; the FastAPI `SSRRenderer` resolves a component path
|
|
against `dirs`, drives the bridge, and wraps the result with the hydration script
|
|
the client reads on mount.
|
|
|
|
Bun is not assumed present in CI, so the bridge is driven against a stand-in
|
|
worker that speaks the SAME newline-delimited JSON-RPC protocol (ready signal +
|
|
`render` → `{id, html}`). That exercises the real bridge code path (spawn,
|
|
message-ID correlation, threaded reader) — only the renderer binary is swapped.
|
|
The path-resolution and hydration-wrapping are tested directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
from mizan_core.ssr import SSRBridge
|
|
from mizan_fastapi.ssr import SSRRenderer
|
|
|
|
|
|
# A Python stand-in for the Bun worker: emits the ready signal, then for each
|
|
# render request echoes a deterministic HTML fragment built from the props.
|
|
_FAKE_WORKER = textwrap.dedent(
|
|
"""
|
|
import json, sys
|
|
sys.stdout.write(json.dumps({"id": 0, "ready": True}) + "\\n"); sys.stdout.flush()
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
msg = json.loads(line)
|
|
if msg.get("method") == "render":
|
|
props = msg["params"]["props"]
|
|
html = "<p>" + props.get("name", "") + "</p>"
|
|
sys.stdout.write(json.dumps({"id": msg["id"], "html": html}) + "\\n")
|
|
sys.stdout.flush()
|
|
"""
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_worker(tmp_path):
|
|
worker = tmp_path / "fake_worker.py"
|
|
worker.write_text(_FAKE_WORKER, encoding="utf-8")
|
|
return str(worker)
|
|
|
|
|
|
@pytest.fixture
|
|
def python_bridge(fake_worker, monkeypatch):
|
|
"""An `SSRBridge` whose subprocess is python (not bun), driving the fake worker."""
|
|
import subprocess
|
|
|
|
real_popen = subprocess.Popen
|
|
|
|
def fake_popen(cmd, *args, **kwargs):
|
|
# Swap the `bun run <worker>` invocation for `python <worker>`.
|
|
if cmd[:2] == ["bun", "run"]:
|
|
cmd = [sys.executable, cmd[2]]
|
|
return real_popen(cmd, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
|
bridge = SSRBridge(worker_path=fake_worker, timeout=5.0)
|
|
yield bridge
|
|
bridge.shutdown()
|
|
|
|
|
|
def test_bridge_round_trips_render(python_bridge):
|
|
result = python_bridge.render("/abs/Hello.tsx", {"name": "World"})
|
|
assert result.html == "<p>World</p>"
|
|
|
|
|
|
def test_bridge_correlates_concurrent_renders(python_bridge):
|
|
# Two renders on the persistent subprocess return their own results.
|
|
a = python_bridge.render("/abs/A.tsx", {"name": "A"})
|
|
b = python_bridge.render("/abs/B.tsx", {"name": "B"})
|
|
assert (a.html, b.html) == ("<p>A</p>", "<p>B</p>")
|
|
|
|
|
|
def test_renderer_resolves_against_dirs_and_wraps_hydration(fake_worker, monkeypatch, tmp_path):
|
|
import subprocess
|
|
|
|
real_popen = subprocess.Popen
|
|
monkeypatch.setattr(
|
|
subprocess, "Popen",
|
|
lambda cmd, *a, **k: real_popen([sys.executable, cmd[2]] if cmd[:2] == ["bun", "run"] else cmd, *a, **k),
|
|
)
|
|
|
|
components = tmp_path / "frontend"
|
|
components.mkdir()
|
|
(components / "Hello.tsx").write_text("export default () => null", encoding="utf-8")
|
|
|
|
renderer = SSRRenderer(worker=fake_worker, dirs=[str(components)])
|
|
try:
|
|
html = renderer.render_to_string("Hello.tsx", {"name": "Mizan"})
|
|
finally:
|
|
renderer.shutdown()
|
|
|
|
assert '<div id="mizan-root"><p>Mizan</p></div>' in html
|
|
assert 'window.__MIZAN_SSR_DATA__={"name": "Mizan"}' in html
|
|
|
|
|
|
def test_renderer_returns_html_response(fake_worker, monkeypatch, tmp_path):
|
|
import subprocess
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
real_popen = subprocess.Popen
|
|
monkeypatch.setattr(
|
|
subprocess, "Popen",
|
|
lambda cmd, *a, **k: real_popen([sys.executable, cmd[2]] if cmd[:2] == ["bun", "run"] else cmd, *a, **k),
|
|
)
|
|
|
|
components = tmp_path / "frontend"
|
|
components.mkdir()
|
|
(components / "Card.tsx").write_text("export default () => null", encoding="utf-8")
|
|
|
|
renderer = SSRRenderer(worker=fake_worker, dirs=[str(components)])
|
|
try:
|
|
response = renderer.render("Card.tsx", {"name": "x"})
|
|
finally:
|
|
renderer.shutdown()
|
|
|
|
assert isinstance(response, HTMLResponse)
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_renderer_raises_on_missing_component(fake_worker, tmp_path):
|
|
renderer = SSRRenderer(worker=fake_worker, dirs=[str(tmp_path)])
|
|
try:
|
|
with pytest.raises(FileNotFoundError):
|
|
renderer.render_to_string("Nope.tsx", {})
|
|
finally:
|
|
renderer.shutdown()
|