AFI parity: close all 35 gaps — every adapter wires every AFI-common capability
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>
This commit is contained in:
138
backends/mizan-fastapi/tests/test_ssr.py
Normal file
138
backends/mizan-fastapi/tests/test_ssr.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user