//! 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":""}` — 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 `. 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 = "" % (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#""# ); // 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#""# ); 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); }