use infinity_config::ProjectConfig; use infinity_host::{HostApiPort, SimulationHostService}; use infinity_host_api::HostApiServer; use serde_json::Value; use std::collections::HashMap; use std::io::{Read, Write}; use std::net::{Shutdown, SocketAddr, TcpStream}; use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; fn sample_project() -> ProjectConfig { ProjectConfig::from_toml_str(include_str!("../../../config/project.example.toml")) .expect("project config must parse") } fn start_server() -> HostApiServer { let service: Arc = Arc::new(SimulationHostService::new(sample_project())); HostApiServer::bind("127.0.0.1:0", service).expect("server must bind") } fn start_server_with_runtime_state(path: &PathBuf) -> HostApiServer { let service: Arc = Arc::new( SimulationHostService::try_new_with_persistence(sample_project(), path) .expect("persistent service must initialize"), ); HostApiServer::bind("127.0.0.1:0", service).expect("server must bind") } struct HttpResponse { status_code: u16, headers: HashMap, body: String, } #[test] fn root_and_web_assets_target_the_versioned_api_contract() { let server = start_server(); let html = send_http_request(server.local_addr(), "GET", "/", None); let technical_html = send_http_request(server.local_addr(), "GET", "/technical", None); let app_js = send_http_request(server.local_addr(), "GET", "/app.js", None); let technical_js = send_http_request(server.local_addr(), "GET", "/technical.js", None); let styles = send_http_request(server.local_addr(), "GET", "/styles.css", None); assert_eq!(html.status_code, 200); assert!(html .headers .get("content-type") .expect("content-type header") .starts_with("text/html")); assert!(html.body.contains("Mapping Settings")); assert!(html.body.contains("

Preview

")); assert!(html.body.contains("Creative Snapshots")); assert!(html.body.contains("Selected Tile")); assert!(html.body.contains("Utilities")); assert!(html.body.contains("View & Output")); assert!(html.body.contains("Pending Transition")); assert!(html.body.contains("Trigger Transition")); assert!(html.body.contains("session-scope-label")); assert!(html.body.contains("Fade Go")); assert!(html.body.contains("Status & Eventfeed")); assert!(html.body.contains("Mapping Settings")); assert!(!html.body.contains("

Groups

")); assert!(html.body.contains("work-mode-select")); assert!(html.body.contains("LEDs Only")); assert!(!html.body.contains("preview-mode-select")); assert_eq!(technical_html.status_code, 200); assert!(technical_html .body .contains("Infinity Vis Mapping Settings")); assert!(technical_html.body.contains("Backend & Output")); assert!(technical_html.body.contains("Node / IP Discovery")); assert!(technical_html.body.contains("Discover / Scan")); assert!(technical_html.body.contains("Node Targets")); assert!(technical_html.body.contains("Panel Mapping")); assert!(technical_html.body.contains("DDP (WLED)")); assert!(technical_html.body.contains("Preview Only")); assert!(technical_html.body.contains("Creative Surface")); assert_eq!(app_js.status_code, 200); assert!(app_js.body.contains("/api/v1/state")); assert!(app_js.body.contains("/api/v1/preview")); assert!(app_js.body.contains("save_preset")); assert!(app_js.body.contains("delete_preset")); assert!(app_js.body.contains("save_creative_snapshot")); assert!(app_js.body.contains("show_control_session_required")); assert!(app_js.body.contains("trigger_transition")); assert!(app_js.body.contains("trigger_panel_test")); assert!(app_js.body.contains("commitState")); assert!(app_js.body.contains("Center Pulse")); assert!(app_js.body.contains("Checkerd")); assert!(app_js.body.contains("Wave Line")); assert!(app_js.body.contains("tile-led")); assert!(app_js.body.contains("show_event")); assert!(app_js.body.contains("test_edit")); assert!(app_js.body.contains("direct_mode_active")); assert!(!app_js.body.contains("Tile Colors")); assert!(!app_js.body.contains("Technical")); assert_eq!(technical_js.status_code, 200); assert!(technical_js.body.contains("/api/v1/state")); assert!(technical_js.body.contains("set_output_backend_mode")); assert!(technical_js.body.contains("set_live_output_enabled")); assert!(technical_js.body.contains("set_output_fps")); assert!(technical_js.body.contains("set_node_reserved_ip")); assert!(technical_js.body.contains("update_panel_mapping")); assert!(technical_js.body.contains("/api/v1/discovery/scan")); assert!(technical_js.body.contains("runDiscoveryScan")); assert!(technical_js.body.contains("DDP (WLED)")); assert_eq!(styles.status_code, 200); assert!(styles .headers .get("content-type") .expect("content-type header") .starts_with("text/css")); assert!(styles.body.contains(".preview-grid")); assert!(styles.body.contains(".tile-led-ring")); assert!(styles.body.contains(".technical-workspace")); assert!(styles.body.contains(".technical-table")); server.shutdown(); } #[test] fn web_ui_browser_smoke_serves_shell_assets_and_stream_bootstrap() { let server = start_server(); let html = send_http_request(server.local_addr(), "GET", "/", None); let mut stream = open_websocket(server.local_addr()); assert_eq!(html.status_code, 200); assert!(html.body.contains("Mapping Settings")); assert!(html.body.contains("

Preview

")); assert!(html.body.contains("connection-pill")); assert!(html.body.contains("preview-grid")); assert!(html.body.contains("workspace-stage")); let first_frame = read_websocket_text_frame(&mut stream); let second_frame = read_websocket_text_frame(&mut stream); let first_payload: Value = serde_json::from_str(&first_frame).expect("first ws frame"); let second_payload: Value = serde_json::from_str(&second_frame).expect("second ws frame"); assert_eq!(first_payload["message"]["type"], "snapshot"); assert_eq!(second_payload["message"]["type"], "preview"); let _ = stream.shutdown(Shutdown::Both); server.shutdown(); } #[test] fn technical_surface_script_guards_missing_recent_events_in_state_snapshot() { let server = start_server(); let technical_js = send_http_request(server.local_addr(), "GET", "/technical.js", None); let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); assert_eq!(technical_js.status_code, 200); assert!(technical_js .body .contains("function snapshotRecentEvents(snapshot)")); assert!(technical_js .body .contains("Array.isArray(snapshot?.recent_events) ? snapshot.recent_events : []")); assert!(technical_js .body .contains("const recentEvents = snapshotRecentEvents(appState.snapshot);")); assert!(state_body["state"].get("recent_events").is_none()); server.shutdown(); } #[test] fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() { let server = start_server(); let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None); let snapshot = send_http_request(server.local_addr(), "GET", "/api/v1/snapshot", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json"); let snapshot_body: Value = serde_json::from_str(&snapshot.body).expect("snapshot json"); assert_eq!(state.status_code, 200); assert_eq!(state_body["api_version"], "v1"); assert!(state_body.get("state").is_some()); assert!(state_body.get("preview").is_none()); assert_eq!( state_body["state"]["technical"]["backend_mode"], "preview_only" ); assert_eq!(state_body["state"]["technical"]["output_enabled"], false); assert_eq!(state_body["state"]["technical"]["output_fps"], 40); assert_eq!( state_body["state"]["nodes"].as_array().map(Vec::len), Some(6) ); assert!(state_body["state"]["nodes"] .as_array() .expect("nodes array") .iter() .all(|node| node["connection"] == "offline" && node["error_status"] == "preview only - live output disabled")); assert!(state_body["state"]["panels"] .as_array() .expect("panels array") .iter() .all(|panel| panel["connection"] == "offline" && panel["driver_kind"] == "pending_validation")); assert_eq!(preview.status_code, 200); assert_eq!(preview_body["api_version"], "v1"); assert!(preview_body.get("preview").is_some()); assert!(preview_body.get("state").is_none()); assert_eq!( preview_body["preview"]["panels"].as_array().map(Vec::len), Some(18) ); assert_eq!(snapshot.status_code, 200); assert_eq!(snapshot_body["api_version"], "v1"); assert!(snapshot_body.get("state").is_some()); assert!(snapshot_body.get("preview").is_some()); server.shutdown(); } #[test] fn technical_surface_commands_update_backend_node_targets_and_panel_mapping() { let server = start_server(); let responses = [ send_command_json( server.local_addr(), r#"{"command":{"type":"set_output_backend_mode","payload":{"mode":"ddp_wled"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":true}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_output_fps","payload":{"output_fps":55}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_node_reserved_ip","payload":{"node_id":"node-01","reserved_ip":"192.168.40.151"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"update_panel_mapping","payload":{"node_id":"node-01","panel_position":"top","physical_output_name":"GPIO 18","driver_kind":"gpio","driver_reference":"GPIO18","led_count":120,"direction":"reverse","color_order":"rgb","enabled":false}}}"#, ), ]; for response in responses { assert_eq!(response.status_code, 200); } let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); assert_eq!(state_body["state"]["technical"]["backend_mode"], "ddp_wled"); assert_eq!(state_body["state"]["technical"]["output_enabled"], true); assert_eq!(state_body["state"]["technical"]["output_fps"], 55); assert_eq!( state_body["state"]["technical"]["live_status"], "DDP (WLED) armed - 0/6 nodes online" ); assert!(state_body["state"]["nodes"] .as_array() .expect("nodes array") .iter() .any(|node| node["node_id"] == "node-01" && node["reserved_ip"] == "192.168.40.151" && node["error_status"] == "ddp (wled) output enabled - no live client connected")); assert!(state_body["state"]["panels"] .as_array() .expect("panels array") .iter() .any(|panel| panel["node_id"] == "node-01" && panel["panel_position"] == "top" && panel["physical_output_name"] == "GPIO 18" && panel["driver_kind"] == "gpio" && panel["driver_reference"] == "GPIO18" && panel["led_count"] == 120 && panel["direction"] == "reverse" && panel["color_order"] == "rgb" && panel["enabled"] == false && panel["error_status"] == "output disabled")); server.shutdown(); } #[test] fn technical_surface_can_disable_output_again_after_enabling_it() { let server = start_server(); let enable_mode = send_command_json( server.local_addr(), r#"{"command":{"type":"set_output_backend_mode","payload":{"mode":"ddp_wled"}}}"#, ); let enable_output = send_command_json( server.local_addr(), r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":true}}}"#, ); let disable_output = send_command_json( server.local_addr(), r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":false}}}"#, ); assert_eq!(enable_mode.status_code, 200); assert_eq!(enable_output.status_code, 200); assert_eq!(disable_output.status_code, 200); let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); assert_eq!(state_body["state"]["technical"]["backend_mode"], "ddp_wled"); assert_eq!(state_body["state"]["technical"]["output_enabled"], false); assert_eq!( state_body["state"]["technical"]["live_status"], "DDP (WLED) selected - output disabled" ); server.shutdown(); } #[test] fn discovery_scan_endpoint_returns_structured_results_and_rejects_invalid_subnets() { let server = start_server(); let invalid = send_http_request( server.local_addr(), "POST", "/api/v1/discovery/scan", Some(r#"{"subnet":"192.168.40.0"}"#), ); let invalid_body: Value = serde_json::from_str(&invalid.body).expect("invalid subnet json"); assert_eq!(invalid.status_code, 400); assert_eq!(invalid_body["error"]["code"], "invalid_subnet_cidr"); let valid = send_http_request( server.local_addr(), "POST", "/api/v1/discovery/scan", Some(r#"{"subnet":"192.168.40.0/30"}"#), ); let valid_body: Value = serde_json::from_str(&valid.body).expect("valid subnet json"); assert_eq!(valid.status_code, 200); assert_eq!(valid_body["api_version"], "v1"); assert_eq!(valid_body["subnet"], "192.168.40.0/30"); assert_eq!(valid_body["scanned_hosts"], 2); assert!(valid_body["results"].is_array()); if let Some(first) = valid_body["results"] .as_array() .and_then(|items| items.first()) { assert!(first.get("ip").is_some()); assert!(first.get("reachable").is_some()); assert!(first.get("detected_type").is_some()); } server.shutdown(); } #[test] fn command_flow_updates_group_parameters_transition_and_blackout() { let server = start_server(); let responses = [ send_command_json( server.local_addr(), r#"{"command":{"type":"select_group","payload":{"group_id":"top_panels"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_scene_parameter","payload":{"key":"speed","value":{"kind":"scalar","value":2.25}}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_transition_style","payload":{"style":"chase"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_transition_duration_ms","payload":{"duration_ms":320}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"gradient"}}}"#, ), ]; for response in responses { assert_eq!(response.status_code, 200); } let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); assert_eq!( state_body["state"]["global"]["selected_group"], "top_panels" ); assert_eq!(state_body["state"]["global"]["transition_style"], "chase"); assert_eq!(state_body["state"]["global"]["transition_duration_ms"], 320); assert_eq!( state_body["state"]["active_scene"]["pattern_id"], "gradient" ); assert!(state_body["state"]["active_scene"]["parameters"] .as_array() .expect("parameter array") .iter() .any(|parameter| parameter["key"] == "speed" && parameter["value"]["value"] == 2.25)); let blackout = send_command_json( server.local_addr(), r#"{"command":{"type":"set_blackout","payload":{"enabled":true}}}"#, ); let blackout_body: Value = serde_json::from_str(&blackout.body).expect("blackout json"); assert_eq!(blackout.status_code, 200); assert_eq!(blackout_body["command_type"], "set_blackout"); let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None); let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json"); assert!(preview_body["preview"]["panels"] .as_array() .expect("preview panels") .iter() .all(|panel| panel["energy_percent"] == 0 && panel["source"] == "blackout")); server.shutdown(); } #[test] fn presets_and_creative_snapshots_persist_across_restart() { let runtime_state_path = unique_runtime_state_path("persistence"); let server = start_server_with_runtime_state(&runtime_state_path); let _ = send_command_json( server.local_addr(), r#"{"command":{"type":"select_group","payload":{"group_id":"bottom_panels"}}}"#, ); let _ = send_command_json( server.local_addr(), r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"noise"}}}"#, ); let _ = send_command_json( server.local_addr(), r#"{"command":{"type":"set_scene_parameter","payload":{"key":"grain","value":{"kind":"scalar","value":0.93}}}}"#, ); let save_preset = send_command_json( server.local_addr(), r#"{"command":{"type":"save_preset","payload":{"preset_id":"user_noise_floor","overwrite":false}}}"#, ); let save_snapshot = send_command_json( server.local_addr(), r#"{"command":{"type":"save_creative_snapshot","payload":{"snapshot_id":"variant_floor","label":"Variant Floor","overwrite":false}}}"#, ); assert_eq!(save_preset.status_code, 200); assert_eq!(save_snapshot.status_code, 200); server.shutdown(); let restarted = start_server_with_runtime_state(&runtime_state_path); let catalog = send_http_request(restarted.local_addr(), "GET", "/api/v1/catalog", None); let state = send_http_request(restarted.local_addr(), "GET", "/api/v1/state", None); let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json"); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); assert!(catalog_body["presets"] .as_array() .expect("preset array") .iter() .any(|preset| preset["preset_id"] == "user_noise_floor" && preset["source"] == "runtime_user")); assert!(catalog_body["creative_snapshots"] .as_array() .expect("snapshot array") .iter() .any(|snapshot| snapshot["snapshot_id"] == "variant_floor")); assert_eq!(state_body["state"]["active_scene"]["pattern_id"], "noise"); assert_eq!( state_body["state"]["active_scene"]["target_group"], "bottom_panels" ); assert!(state_body["state"]["active_scene"]["parameters"] .as_array() .expect("parameter array") .iter() .any(|parameter| parameter["key"] == "grain" && parameter["value"]["value"] == 0.93)); restarted.shutdown(); let _ = std::fs::remove_file(runtime_state_path); } #[test] fn runtime_presets_can_be_deleted_but_builtin_presets_are_protected() { let server = start_server(); let save = send_command_json( server.local_addr(), r#"{"command":{"type":"save_preset","payload":{"preset_id":"runtime_delete_me","overwrite":false}}}"#, ); assert_eq!(save.status_code, 200); let delete_runtime = send_command_json( server.local_addr(), r#"{"command":{"type":"delete_preset","payload":{"preset_id":"runtime_delete_me"}}}"#, ); assert_eq!(delete_runtime.status_code, 200); let catalog_after_delete = send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None); let catalog_body: Value = serde_json::from_str(&catalog_after_delete.body).expect("catalog json"); assert!(!catalog_body["presets"] .as_array() .expect("preset array") .iter() .any(|preset| preset["preset_id"] == "runtime_delete_me")); let delete_builtin = send_command_json( server.local_addr(), r#"{"command":{"type":"delete_preset","payload":{"preset_id":"ocean_gradient"}}}"#, ); let delete_builtin_body: Value = serde_json::from_str(&delete_builtin.body).expect("delete builtin json"); assert_eq!(delete_builtin.status_code, 400); assert_eq!( delete_builtin_body["error"]["code"], "preset_delete_forbidden" ); server.shutdown(); } #[test] fn show_control_flows_cover_runtime_group_preset_snapshot_transition_blackout_and_eventfeed() { let server = start_server(); let mut stream = open_websocket(server.local_addr()); let _ = read_websocket_text_frame(&mut stream); let _ = read_websocket_text_frame(&mut stream); let flow_responses = [ send_command_json( server.local_addr(), r#"{"command":{"type":"upsert_group","payload":{"group_id":"focus_pair","tags":["runtime","focus"],"members":[{"node_id":"node-a","panel_position":"top"},{"node_id":"node-a","panel_position":"middle"}],"overwrite":false}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"select_group","payload":{"group_id":"focus_pair"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_transition_style","payload":{"style":"chase"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_transition_duration_ms","payload":{"duration_ms":480}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"noise"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_scene_parameter","payload":{"key":"grain","value":{"kind":"scalar","value":0.67}}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"save_preset","payload":{"preset_id":"focus_noise","overwrite":false}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_scene_parameter","payload":{"key":"grain","value":{"kind":"scalar","value":0.81}}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"save_preset","payload":{"preset_id":"focus_noise","overwrite":true}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"recall_preset","payload":{"preset_id":"focus_noise"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"save_creative_snapshot","payload":{"snapshot_id":"focus_variant","label":"Focus Variant","overwrite":false}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"select_pattern","payload":{"pattern_id":"pulse"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"recall_creative_snapshot","payload":{"snapshot_id":"focus_variant"}}}"#, ), send_command_json( server.local_addr(), r#"{"command":{"type":"set_blackout","payload":{"enabled":true}}}"#, ), ]; for response in flow_responses { assert_eq!(response.status_code, 200); } let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None); let catalog = send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json"); let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json"); assert_eq!( state_body["state"]["global"]["selected_group"], "focus_pair" ); assert_eq!(state_body["state"]["global"]["transition_style"], "chase"); assert_eq!(state_body["state"]["global"]["transition_duration_ms"], 480); assert_eq!(state_body["state"]["global"]["blackout"], true); assert_eq!(state_body["state"]["active_scene"]["pattern_id"], "noise"); assert_eq!( state_body["state"]["active_scene"]["target_group"], "focus_pair" ); assert!(state_body["state"]["active_scene"]["parameters"] .as_array() .expect("parameter array") .iter() .any(|parameter| parameter["key"] == "grain" && parameter["value"]["value"] == 0.81)); assert!(catalog_body["groups"] .as_array() .expect("group array") .iter() .any(|group| group["group_id"] == "focus_pair" && group["source"] == "runtime_user")); assert!(catalog_body["presets"] .as_array() .expect("preset array") .iter() .any(|preset| preset["preset_id"] == "focus_noise" && preset["source"] == "runtime_user" && preset["transition_style"] == "chase")); assert!(catalog_body["creative_snapshots"] .as_array() .expect("snapshot array") .iter() .any(|snapshot| snapshot["snapshot_id"] == "focus_variant" && snapshot["label"] == "Focus Variant")); assert!(preview_body["preview"]["panels"] .as_array() .expect("preview panels") .iter() .all(|panel| panel["energy_percent"] == 0 && panel["source"] == "blackout")); let mut event_messages = Vec::new(); for _ in 0..24 { let frame = read_websocket_text_frame(&mut stream); let payload: Value = serde_json::from_str(&frame).expect("ws frame"); if payload["message"]["type"] == "event" { if let Some(message) = payload["message"]["payload"]["message"].as_str() { event_messages.push(message.to_string()); } } } assert!(event_messages .iter() .any(|message| message.contains("group saved: focus_pair"))); assert!(event_messages .iter() .any(|message| message.contains("preset overwritten: focus_noise"))); assert!(event_messages .iter() .any(|message| message.contains("creative snapshot recalled: focus_variant"))); assert!(event_messages .iter() .any(|message| message.contains("global blackout enabled"))); let _ = stream.shutdown(Shutdown::Both); server.shutdown(); } #[test] fn invalid_runtime_state_file_falls_back_without_blocking_server_start() { let runtime_state_path = unique_runtime_state_path("invalid_runtime"); std::fs::write(&runtime_state_path, "{ broken").expect("invalid runtime state should write"); let server = start_server_with_runtime_state(&runtime_state_path); let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); let mut stream = open_websocket(server.local_addr()); assert_eq!(state.status_code, 200); assert_eq!( state_body["state"]["active_scene"]["pattern_id"], "solid_color" ); let _ = read_websocket_text_frame(&mut stream); let _ = read_websocket_text_frame(&mut stream); let mut saw_recovery_warning = false; for _ in 0..8 { let frame = read_websocket_text_frame(&mut stream); let payload: Value = serde_json::from_str(&frame).expect("ws frame"); if payload["message"]["type"] == "event" && payload["message"]["payload"]["code"] == "runtime_state_parse_failed" { saw_recovery_warning = true; assert_eq!(payload["message"]["payload"]["kind"], "warning"); break; } } assert!( saw_recovery_warning, "expected recovery warning event after invalid runtime state" ); let _ = stream.shutdown(Shutdown::Both); server.shutdown(); let _ = std::fs::remove_file(runtime_state_path); } #[test] fn websocket_stream_reports_event_codes_and_command_failures_stay_typed() { let server = start_server(); let mut stream = open_websocket(server.local_addr()); let first_frame = read_websocket_text_frame(&mut stream); let first_payload: Value = serde_json::from_str(&first_frame).expect("first ws frame"); assert_eq!(first_payload["message"]["type"], "snapshot"); let second_frame = read_websocket_text_frame(&mut stream); let second_payload: Value = serde_json::from_str(&second_frame).expect("second ws frame"); assert_eq!(second_payload["message"]["type"], "preview"); let invalid = send_command_json( server.local_addr(), r#"{"command":{"type":"recall_creative_snapshot","payload":{"snapshot_id":"does_not_exist"}}}"#, ); let invalid_body: Value = serde_json::from_str(&invalid.body).expect("invalid json"); assert_eq!(invalid.status_code, 400); assert_eq!(invalid_body["error"]["code"], "unknown_creative_snapshot"); let mut saw_warning = false; for _ in 0..12 { let frame = read_websocket_text_frame(&mut stream); let payload: Value = serde_json::from_str(&frame).expect("ws frame"); if payload["message"]["type"] == "event" && payload["message"]["payload"]["code"] == "unknown_creative_snapshot" { saw_warning = true; assert_eq!(payload["message"]["payload"]["kind"], "warning"); assert_eq!( payload["message"]["payload"]["code"], "unknown_creative_snapshot" ); assert!(payload["message"]["payload"]["message"] .as_str() .expect("event message") .contains("does_not_exist")); break; } } assert!(saw_warning, "expected warning event after failed command"); let _ = stream.shutdown(Shutdown::Both); server.shutdown(); } #[test] #[ignore = "longer load-oriented sequence for platform hardening"] fn load_sequence_keeps_state_preview_and_catalog_consistent() { let server = start_server(); let patterns = ["solid_color", "gradient", "chase", "pulse", "noise"]; let groups = [ None, Some("top_panels"), Some("middle_panels"), Some("bottom_panels"), ]; for index in 0..80 { let pattern = patterns[index % patterns.len()]; let group = groups[index % groups.len()]; let brightness = ((index % 10) as f32) / 10.0; let speed = 0.5 + (index % 6) as f32 * 0.25; let _ = send_command_json( server.local_addr(), &format!( r#"{{"command":{{"type":"select_pattern","payload":{{"pattern_id":"{pattern}"}}}}}}"# ), ); let _ = send_command_json( server.local_addr(), &format!( r#"{{"command":{{"type":"set_master_brightness","payload":{{"value":{brightness}}}}}}}"# ), ); let _ = send_command_json( server.local_addr(), &format!( r#"{{"command":{{"type":"set_scene_parameter","payload":{{"key":"speed","value":{{"kind":"scalar","value":{speed}}}}}}}}}"# ), ); let group_json = match group { Some(group_id) => format!(r#""{group_id}""#), None => "null".to_string(), }; let _ = send_command_json( server.local_addr(), &format!( r#"{{"command":{{"type":"select_group","payload":{{"group_id":{group_json}}}}}}}"# ), ); let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None); let preview = send_http_request(server.local_addr(), "GET", "/api/v1/preview", None); let catalog = send_http_request(server.local_addr(), "GET", "/api/v1/catalog", None); let state_body: Value = serde_json::from_str(&state.body).expect("state json"); let preview_body: Value = serde_json::from_str(&preview.body).expect("preview json"); let catalog_body: Value = serde_json::from_str(&catalog.body).expect("catalog json"); assert_eq!( state_body["state"]["panels"].as_array().map(Vec::len), Some(18) ); assert_eq!( preview_body["preview"]["panels"].as_array().map(Vec::len), Some(18) ); assert!( catalog_body["patterns"] .as_array() .map(Vec::len) .unwrap_or_default() >= 5 ); } server.shutdown(); } fn send_command_json(addr: SocketAddr, body: &str) -> HttpResponse { send_http_request(addr, "POST", "/api/v1/command", Some(body)) } fn send_http_request( addr: SocketAddr, method: &str, path: &str, body: Option<&str>, ) -> HttpResponse { let body = body.unwrap_or(""); let request = format!( "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", body.as_bytes().len(), body, host = addr ); let mut stream = TcpStream::connect(addr).expect("http connection"); stream .set_read_timeout(Some(Duration::from_secs(3))) .expect("read timeout"); stream.write_all(request.as_bytes()).expect("write request"); stream.shutdown(Shutdown::Write).expect("shutdown write"); let mut raw = Vec::new(); stream.read_to_end(&mut raw).expect("read response"); parse_http_response(&raw) } fn parse_http_response(raw: &[u8]) -> HttpResponse { let delimiter = raw .windows(4) .position(|window| window == b"\r\n\r\n") .expect("http header delimiter"); let header_text = String::from_utf8(raw[..delimiter].to_vec()).expect("header utf8"); let body = String::from_utf8(raw[delimiter + 4..].to_vec()).expect("body utf8"); let mut lines = header_text.lines(); let status_line = lines.next().expect("status line"); let status_code = status_line .split_whitespace() .nth(1) .expect("status code") .parse::() .expect("valid status code"); let headers = lines .filter_map(|line| line.split_once(':')) .map(|(key, value)| (key.trim().to_ascii_lowercase(), value.trim().to_string())) .collect::>(); HttpResponse { status_code, headers, body, } } fn open_websocket(addr: SocketAddr) -> TcpStream { let mut stream = TcpStream::connect(addr).expect("websocket connection"); stream .set_read_timeout(Some(Duration::from_secs(3))) .expect("read timeout"); let request = format!( "GET /api/v1/stream HTTP/1.1\r\nHost: {host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n", host = addr ); stream .write_all(request.as_bytes()) .expect("write handshake"); let header = read_until_header_end(&mut stream); let header_text = String::from_utf8(header).expect("handshake utf8"); assert!(header_text.starts_with("HTTP/1.1 101")); assert!(header_text.contains("Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")); stream } fn read_until_header_end(stream: &mut TcpStream) -> Vec { let mut buffer = Vec::new(); loop { let mut byte = [0u8; 1]; let read = stream.read(&mut byte).expect("read handshake"); assert!(read > 0, "unexpected eof while reading handshake"); buffer.push(byte[0]); if buffer.windows(4).any(|window| window == b"\r\n\r\n") { let end = buffer .windows(4) .position(|window| window == b"\r\n\r\n") .expect("header end") + 4; return buffer[..end].to_vec(); } } } fn read_websocket_text_frame(stream: &mut TcpStream) -> String { let mut header = [0u8; 2]; stream.read_exact(&mut header).expect("frame header"); assert_eq!(header[0] & 0x0f, 0x1, "expected text frame"); let payload_len = match header[1] & 0x7f { len @ 0..=125 => len as usize, 126 => { let mut extended = [0u8; 2]; stream.read_exact(&mut extended).expect("extended payload"); u16::from_be_bytes(extended) as usize } 127 => { let mut extended = [0u8; 8]; stream.read_exact(&mut extended).expect("extended payload"); u64::from_be_bytes(extended) as usize } _ => unreachable!("masked length bit should already be stripped"), }; let mut payload = vec![0u8; payload_len]; stream.read_exact(&mut payload).expect("frame payload"); String::from_utf8(payload).expect("frame utf8") } fn unique_runtime_state_path(label: &str) -> PathBuf { let millis = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system time") .as_millis(); std::env::temp_dir().join(format!("infinity_vis_{label}_{millis}.json")) }