Stabilize control surface and external bridge v1

This commit is contained in:
jan
2026-04-20 01:13:27 +02:00
parent a56cecb23d
commit 07c52db5fb
29 changed files with 8818 additions and 1510 deletions

View File

@@ -37,7 +37,9 @@ struct HttpResponse {
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);
@@ -46,19 +48,66 @@ fn root_and_web_assets_target_the_versioned_api_contract() {
.get("content-type")
.expect("content-type header")
.starts_with("text/html"));
assert!(html.body.contains("Preset Capture"));
assert!(html.body.contains("Mapping Settings"));
assert!(html.body.contains("<h2>Preview</h2>"));
assert!(html.body.contains("Creative Snapshots"));
assert!(html.body.contains("Event Stream"));
assert!(html.body.contains("Selected Tile"));
assert!(html.body.contains("Utilities"));
assert!(html.body.contains("View &amp; 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 &amp; Eventfeed"));
assert!(html.body.contains("Mapping Settings"));
assert!(!html.body.contains("<h2>Groups</h2>"));
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 &amp; 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
@@ -66,6 +115,9 @@ fn root_and_web_assets_target_the_versioned_api_contract() {
.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();
}
@@ -77,9 +129,11 @@ fn web_ui_browser_smoke_serves_shell_assets_and_stream_bootstrap() {
let mut stream = open_websocket(server.local_addr());
assert_eq!(html.status_code, 200);
assert!(html.body.contains("Infinity Vis"));
assert!(html.body.contains("Mapping Settings"));
assert!(html.body.contains("<h2>Preview</h2>"));
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);
@@ -108,10 +162,28 @@ fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
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");
@@ -130,6 +202,110 @@ fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
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 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();
@@ -260,6 +436,47 @@ fn presets_and_creative_snapshots_persist_across_restart() {
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();