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:
105
cores/mizan-rust/tests/ssr_bridge.rs
Normal file
105
cores/mizan-rust/tests/ssr_bridge.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Behavior test for the SSR bridge's framing + request/response correlation.
|
||||
//!
|
||||
//! Bun isn't required (it isn't installed in CI): a stub worker speaking the
|
||||
//! exact same newline-delimited JSON-RPC protocol stands in. The stub emits
|
||||
//! the `{"id":0,"ready":true}` handshake, then for each `render` request
|
||||
//! echoes back `{"id":N,"html":"<rendered:FILE props=PROPS>"}` — exercising
|
||||
//! the ready-gate, the per-request id correlation, and the html extraction
|
||||
//! that the real Bun worker drives.
|
||||
|
||||
use mizan_core::{SsrBridge, WorkerCommand};
|
||||
use serde_json::json;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A tiny Python stub that speaks the SSR worker protocol. Written to a temp
|
||||
/// file and launched via `python3 <file>`.
|
||||
const STUB: &str = r#"
|
||||
import sys, json
|
||||
# Handshake: announce readiness exactly as the Bun worker does.
|
||||
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)
|
||||
mid = msg.get("id")
|
||||
if msg.get("method") == "render":
|
||||
p = msg["params"]
|
||||
# A sentinel file name forces the worker-error branch.
|
||||
if p["file"] == "/boom.tsx":
|
||||
sys.stdout.write(json.dumps({"id": mid, "error": "render exploded"}) + "\n")
|
||||
else:
|
||||
html = "<rendered:%s props=%s>" % (p["file"], json.dumps(p["props"], sort_keys=True))
|
||||
sys.stdout.write(json.dumps({"id": mid, "html": html}) + "\n")
|
||||
else:
|
||||
sys.stdout.write(json.dumps({"id": mid, "error": "unknown method"}) + "\n")
|
||||
sys.stdout.flush()
|
||||
"#;
|
||||
|
||||
fn write_stub() -> std::path::PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("mizan_ssr_stub_{}.py", std::process::id()));
|
||||
let mut f = std::fs::File::create(&path).unwrap();
|
||||
f.write_all(STUB.as_bytes()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_drives_worker_protocol() {
|
||||
let stub = write_stub();
|
||||
let bridge = SsrBridge::new(
|
||||
WorkerCommand {
|
||||
program: "python3".to_string(),
|
||||
args: vec![stub.to_string_lossy().to_string()],
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
// First render — spawns the worker, waits for the ready handshake.
|
||||
let html = bridge
|
||||
.render("/abs/Hello.tsx", json!({"name": "World"}))
|
||||
.expect("first render succeeds");
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<rendered:/abs/Hello.tsx props={"name": "World"}>"#
|
||||
);
|
||||
|
||||
// Second render reuses the same subprocess; id correlation must keep the
|
||||
// responses matched to their requests.
|
||||
let html2 = bridge
|
||||
.render("/abs/Other.tsx", json!({"a": 1, "b": 2}))
|
||||
.expect("second render succeeds");
|
||||
assert_eq!(
|
||||
html2,
|
||||
r#"<rendered:/abs/Other.tsx props={"a": 1, "b": 2}>"#
|
||||
);
|
||||
|
||||
bridge.shutdown();
|
||||
let _ = std::fs::remove_file(&stub);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_propagates_worker_error() {
|
||||
let stub = write_stub();
|
||||
let bridge = SsrBridge::new(
|
||||
WorkerCommand {
|
||||
program: "python3".to_string(),
|
||||
args: vec![stub.to_string_lossy().to_string()],
|
||||
},
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
// The sentinel file makes the stub return an `error` frame; the bridge
|
||||
// must surface it as `SsrError::Render`, not a successful empty render.
|
||||
let err = bridge
|
||||
.render("/boom.tsx", json!({}))
|
||||
.expect_err("worker error propagates");
|
||||
assert!(matches!(err, mizan_core::SsrError::Render(_)));
|
||||
assert!(err.to_string().contains("render exploded"));
|
||||
|
||||
// A subsequent good render on the same worker still succeeds.
|
||||
assert!(bridge.render("/ok.tsx", json!({})).is_ok());
|
||||
bridge.shutdown();
|
||||
let _ = std::fs::remove_file(&stub);
|
||||
}
|
||||
Reference in New Issue
Block a user