//! Contract tests for server install-time operational defaults. //! //! Scope: statically guard the generated `/etc/lesavka/server.env` values. //! Targets: `scripts/install/server.sh`. //! Why: HDMI capture adapter settings should be reproducible after reboot or //! reinstall instead of living as one-off shell state. const SERVER_INSTALL: &str = include_str!("../../scripts/install/server.sh"); #[test] fn server_install_pins_hdmi_camera_and_display_defaults() { for expected in [ "LESAVKA_CAM_OUTPUT=%s", "LESAVKA_CAM_WIDTH=%s", "LESAVKA_CAM_HEIGHT=%s", "LESAVKA_CAM_FPS=%s", "LESAVKA_HDMI_WIDTH=%s", "LESAVKA_HDMI_HEIGHT=%s", "LESAVKA_HDMI_SINK=%s", "LESAVKA_HDMI_FBDEV=%s", "LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS=%s", "LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS=%s", "LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS=%s", "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=%s", "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s", "LESAVKA_UPSTREAM_PAIR_SLACK_US=%s", "LESAVKA_UPSTREAM_AUDIO_MASTER_WAIT_GRACE_MS=%s", "LESAVKA_UPSTREAM_STALE_DROP_MS=%s", "LESAVKA_SERVER_BIND_ADDR=%s", "/etc/lesavka/uvc.env", "LESAVKA_UVC_MAXPACKET=", "LESAVKA_UVC_INTERVAL=", "LESAVKA_UVC_WIDTH=", "LESAVKA_UVC_HEIGHT=", "LESAVKA_UVC_CODEC=", "LESAVKA_UVC_CONTROL_READ_ONLY=", "LESAVKA_REQUIRE_TLS=%s", "LESAVKA_TLS_CERT=%s", "LESAVKA_TLS_KEY=%s", "LESAVKA_TLS_CLIENT_CA=%s", ] { assert!( SERVER_INSTALL.contains(expected), "install script should emit {expected}" ); } assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_WIDTH:-1920}")); assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_HEIGHT:-1080}")); assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_FPS:-30}")); assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_CAM_OUTPUT:-uvc}")); assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}")); assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_WIDTH:-1920}")); assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_HEIGHT:-1080}")); assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-350}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS:-1000}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS:-60000}")); assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=0")); assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=1090000")); assert!( SERVER_INSTALL.contains("resolve_upstream_video_playout_offset_us"), "video offset should be resolved through stale-baseline migration logic" ); assert!(SERVER_INSTALL.contains("LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000")); assert!( SERVER_INSTALL.contains("PREVIOUS_TUNED_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000") ); assert!(SERVER_INSTALL.contains("PREVIOUS_ZERO_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=0")); assert!( SERVER_INSTALL.contains("PREVIOUS_DELAYED_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=350000") ); assert!( SERVER_INSTALL.contains("PREVIOUS_BROWSER_MJPEG_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=130000") ); assert!( SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"), "install-specific offset override should bypass stale ambient runtime env" ); assert!( SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US"), "install-specific video offset override should bypass stale ambient runtime env" ); assert!( SERVER_INSTALL.contains("migrating stale upstream audio playout offset to the 0.17 freshness-first planner default"), "installer should not preserve old MJPEG/UVC sync baselines accidentally" ); assert!( SERVER_INSTALL.contains("migrating stale upstream video playout offset to the 0.17 browser-visible MJPEG/UVC sync baseline"), "installer should not preserve old video delay baselines accidentally" ); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_AUDIO_MASTER_WAIT_GRACE_MS:-350}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}")); assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_MAXPACKET:-1024}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_INTERVAL:-333333}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_WIDTH:-1280}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEIGHT:-720}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_CONTROL_READ_ONLY:-0}")); assert!( !SERVER_INSTALL.contains("LESAVKA_UVC_CODEC=${LESAVKA_UVC_CODEC:-mjpeg}"), "install script should not let ambient LESAVKA_UVC_CODEC leak into persisted defaults" ); assert!( !SERVER_INSTALL .contains("LESAVKA_SERVER_BIND_ADDR=${LESAVKA_SERVER_BIND_ADDR:-0.0.0.0:50051}"), "install script should not let ambient LESAVKA_SERVER_BIND_ADDR leak into persisted defaults" ); assert!( SERVER_INSTALL.contains("EnvironmentFile=-/etc/lesavka/uvc.env"), "install script should feed live UVC runtime settings into services" ); assert!( !SERVER_INSTALL.contains("Environment=LESAVKA_UVC_CODEC=mjpeg"), "install script should not pin UVC codec to mjpeg in the systemd units" ); assert!( SERVER_INSTALL.contains("LESAVKA_UVC_FALLBACK=0"), "forced gadget rebuilds should fail loud instead of silently dropping UVC" ); assert!( SERVER_INSTALL.contains("validate_uvc_gadget_ready"), "install script should verify that the UVC gadget comes back before declaring success" ); assert!( SERVER_INSTALL.contains("uvc_gadget_present"), "install script should detect when the live gadget is missing the expected UVC function" ); assert!( SERVER_INSTALL.contains( "UVC function is missing from the live gadget; forcing a rebuild before server start." ), "install script should force a rebuild when the live gadget is attached but missing UVC" ); assert!( SERVER_INSTALL.contains("lesavka-core gadget rebuilt directly."), "install script should trust the direct forced rebuild instead of immediately rerunning the oneshot core unit" ); assert!( SERVER_INSTALL.contains("LESAVKA_FORCE_GADGET_REBUILD"), "install script should require an explicit force knob for attached-host gadget rebuilds" ); assert!( SERVER_INSTALL.contains("EXPLICIT_GADGET_REBUILD=1"), "installer should distinguish organic rebuild needs from an explicit operator hard-reset request" ); assert!( SERVER_INSTALL.contains("[[ \"$EXPLICIT_GADGET_REBUILD\" != \"1\" ]] || [[ -z ${LESAVKA_ALLOW_GADGET_RESET:-} ]]"), "attached-host hard rebuilds should require both allow and explicit force flags" ); assert!( SERVER_INSTALL.contains("no hard gadget rebuild is needed"), "LESAVKA_ALLOW_GADGET_RESET should permit recovery without causing unconditional hard rebuilds" ); assert!( !SERVER_INSTALL.contains( "[[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || [[ \"$FORCE_GADGET_REBUILD\" == \"1\" ]]" ), "LESAVKA_ALLOW_GADGET_RESET must not itself trigger a hard UDC detach/rebind" ); assert!( !SERVER_INSTALL.contains("LESAVKA_RELOAD_UVCVIDEO=1"), "install script should not unload/reload uvcvideo during ordinary server installs" ); assert!( SERVER_INSTALL.contains("sudo systemctl stop lesavka-uvc"), "install script should stop the UVC helper before directly rebuilding the gadget underneath it" ); assert!( !SERVER_INSTALL.contains("sudo systemctl restart lesavka-core"), "install script should not immediately rerun lesavka-core after a successful direct forced rebuild" ); assert!( SERVER_INSTALL.contains("video-output node did not appear after rebuild"), "install script should explain why it refuses a half-applied UVC install" ); assert!( !SERVER_INSTALL.contains("RefuseManualStop=yes"), "install script should not generate a UVC unit that blocks legitimate refreshes" ); assert!( SERVER_INSTALL.contains("KillMode=control-group"), "install script should stop the whole UVC helper cgroup instead of leaving child processes behind" ); assert!( !SERVER_INSTALL.contains("KillMode=process"), "install script should not leave the server or helper units in process-only kill mode" ); assert!( SERVER_INSTALL.contains("lesavka-uvc already active; runtime settings unchanged."), "install script should avoid unnecessary UVC restarts when nothing changed" ); assert!( SERVER_INSTALL.contains("sudo systemctl start lesavka-uvc"), "install script should start the UVC helper so the host enumerates the UVC function" ); assert!( SERVER_INSTALL.contains("lesavka-uvc started to attach the UVC gadget to the host."), "install script should report when it starts the UVC helper for enumeration" ); assert!( SERVER_INSTALL.contains("Wants=lesavka-uvc.service"), "server unit should pull in the external UVC helper on UVC installs" ); assert!( !SERVER_INSTALL.contains("Environment=LESAVKA_ALLOW_GADGET_CYCLE=1"), "server unit should not auto-cycle the gadget while the external UVC helper owns it" ); assert!( SERVER_INSTALL.contains("/var/log/lesavka/server.log"), "install script should keep server logs out of sticky /tmp" ); assert!( SERVER_INSTALL.contains("LESAVKA_SERVER_LOG_PATH=/var/log/lesavka/server.log"), "server unit should point tracing at the non-sticky log path" ); assert!( SERVER_INSTALL.contains("clear_stale_server_listener"), "install script should clear stale server listeners before restart" ); assert!( SERVER_INSTALL.contains("lsof -tiTCP:"), "install script should inspect the bind port before starting a fresh server" ); assert!( SERVER_INSTALL.contains("/proc/net/tcp"), "install script should fall back to procfs listener detection when lsof misses the owner" ); assert!( SERVER_INSTALL.contains("socket:["), "install script should map listening socket inodes back to owning PIDs" ); assert!( SERVER_INSTALL.contains("ss -K state connected"), "install script should clear hidden connected socket state before retrying the server port" ); assert!( SERVER_INSTALL.contains("ss -K state listening"), "install script should ask the kernel to drop a hidden listening socket when no PID is visible" ); assert!( SERVER_INSTALL.contains("list_server_bound_inactive_lines"), "install script should check for sockets that remain bound after listener teardown" ); assert!( SERVER_INSTALL.contains("bound-inactive tcp sockets"), "install script should explain when a hidden socket stops listening but still blocks the port" ); assert!( SERVER_INSTALL.contains("unexpected process"), "install script should fail loud instead of killing an unrelated process on the server port" ); assert!( SERVER_INSTALL.contains("kernel dropped the hidden TCP"), "install script should report when kernel-level stale-socket cleanup succeeds" ); assert!( SERVER_INSTALL.contains("no owning PID could be identified"), "install script should fail loud when a port is listening but procfs cannot identify the owner" ); assert!( SERVER_INSTALL.contains("validate_server_ready"), "install script should verify that lesavka-server reaches a running state" ); assert!( SERVER_INSTALL.contains("edid_bytes=$(wc -c <\"$dev/edid\""), "HDMI auto-detection should count real EDID bytes instead of trusting sysfs file size" ); assert!( SERVER_INSTALL.contains("(( edid_bytes > 0 ))"), "HDMI auto-detection should prefer a connector with real EDID over a boot-forced phantom connection" ); assert!( SERVER_INSTALL.contains("preserving existing LESAVKA_HDMI_CONNECTOR"), "HDMI auto-detection should preserve the prior connector when physical evidence is still tied" ); assert!( SERVER_INSTALL.contains("edid=%sB"), "ambiguous HDMI diagnostics should show EDID evidence for operator debugging" ); assert!( SERVER_INSTALL.contains("active and listening"), "install script should require the TCP listener before declaring server readiness" ); assert!( SERVER_INSTALL.contains("did not open TCP"), "install script should explain active-but-not-listening server failures" ); assert!( SERVER_INSTALL.contains("failed to reach active/running state"), "install script should explain server startup failures instead of claiming success" ); } #[test] fn server_install_generates_mtls_identity_and_client_bundle() { for expected in [ "ensure_server_tls_pki", "openssl genrsa", "extendedKeyUsage = serverAuth", "extendedKeyUsage = clientAuth", "LESAVKA_CLIENT_BUNDLE", "lesavka-client-pki.tar.gz", "Client TLS bundle:", "Client install can use:", "sudo chown \"$ORIG_USER\":\"$ORIG_USER\" \"$LESAVKA_CLIENT_BUNDLE\"", "LESAVKA_REQUIRE_TLS=%s", "LESAVKA_TLS_CLIENT_CA=%s", ] { assert!( SERVER_INSTALL.contains(expected), "server install script should include TLS/mTLS contract fragment {expected}" ); } assert!( SERVER_INSTALL.contains("LESAVKA_REQUIRE_TLS:-1"), "installed servers should require TLS by default" ); assert!( SERVER_INSTALL.contains("38.28.125.112"), "server cert SAN defaults should include the current public relay endpoint" ); } #[test] fn server_install_reports_installed_version_and_revision() { assert!( SERVER_INSTALL.contains("Installed:"), "install script should print the installed build identity at the end" ); assert!( SERVER_INSTALL.contains("manifest_package_version"), "install script should read the package version from the synced source manifest" ); assert!( SERVER_INSTALL.contains("\"$SRC_DIR/server/Cargo.toml\""), "install script should report the server package semver from the server manifest" ); assert!( SERVER_INSTALL.contains("git -C \"$SCRIPT_REPO_ROOT\" rev-parse --short HEAD"), "install script should print the installed git revision for operator clarity" ); assert!( SERVER_INSTALL.contains("Camera output:"), "install script should print the persisted camera output in the footer" ); assert!( SERVER_INSTALL.contains("Server bind:"), "install script should print the persisted server bind address in the footer" ); assert!( SERVER_INSTALL.contains("UVC codec:"), "install script should print the persisted UVC codec in the footer" ); } #[test] fn server_install_keeps_uac_sanity_helper_available() { assert!( SERVER_INSTALL.contains("run_uac_output_sanity.sh"), "install script should ship the root UAC sanity helper" ); assert!( SERVER_INSTALL.contains("/usr/local/bin/lesavka-uac-sanity"), "install script should install the UAC sanity helper to a stable path" ); } #[test] fn server_install_grants_operator_audio_group_access() { assert!( SERVER_INSTALL.contains("usermod -aG audio"), "install script should grant the invoking operator access to ALSA devices" ); assert!( SERVER_INSTALL.contains("SUDO_USER"), "install script should target the sudo-invoking operator instead of a hard-coded account" ); } #[test] fn server_install_builds_eye_links_from_capture_nodes_only() { assert!( SERVER_INSTALL.contains("/sys/class/video4linux/$(basename \"$dev\")/index"), "install script should inspect the kernel video index so it only links real capture nodes" ); assert!( SERVER_INSTALL.contains("GC_CAPTURE_PAIRS"), "install script should assemble eye-link candidates from udev-tagged capture devices" ); assert!( SERVER_INSTALL.contains("discover_gc_capture_pairs"), "install script should retry capture-node discovery instead of trusting a single early snapshot" ); assert!( SERVER_INSTALL.contains("for _ in {1..20}; do"), "install script should wait briefly for both GC311 capture nodes to finish enumerating" ); assert!( SERVER_INSTALL.contains("borrowing relay GPIO power for capture discovery"), "install script should temporarily power capture cards for discovery when relay GPIO is available" ); assert!( SERVER_INSTALL.contains("returning relay GPIO power control to Lesavka auto mode"), "install script should restore relay power control after capture discovery" ); assert!( SERVER_INSTALL.contains("trap restore_capture_power_after_discovery EXIT"), "install script should restore borrowed capture power even if discovery exits early" ); assert!( SERVER_INSTALL.contains("[ \"$LEFT_TAG\" = \"$RIGHT_TAG\" ]"), "install script should refuse duplicated path tags instead of assigning both eyes to one card" ); }