mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
1035 lines
37 KiB
Rust
1035 lines
37 KiB
Rust
use super::*;
|
|
|
|
// Tests that read/write process-global env vars must serialize through this
|
|
// mutex. Rust's test runner executes tests in parallel by default; without
|
|
// coordination, concurrent set_var / remove_var calls race and produce
|
|
// spurious failures.
|
|
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
// NOTE: `is_daemon_mode_detects_daemon_flag` removed (plan.md §2.1) — it
|
|
// discarded the result with `let _` and asserted nothing.
|
|
|
|
// Regression for #4809 + #4818: the restore command is chosen per frame.
|
|
// - A minimized frame (desktop shortcut / taskbar / tray while minimized) must
|
|
// un-minimize the OS frame; `SW_SHOW` leaves it minimized, so it uses
|
|
// `SW_RESTORE` (#4809).
|
|
// - A hidden-but-not-minimized frame may be maximized; `SW_RESTORE` would drop
|
|
// it back to normal size, so it uses `SW_SHOW` to preserve the maximized
|
|
// geometry (#4818).
|
|
// Windows-only: `window_show_command` is `cfg(windows)`.
|
|
#[cfg(target_os = "windows")]
|
|
#[test]
|
|
fn restore_command_preserves_minimized_and_maximized_frames() {
|
|
use windows_sys::Win32::UI::WindowsAndMessaging::{SW_HIDE, SW_RESTORE, SW_SHOW};
|
|
// Minimized restore un-minimizes AND un-hides.
|
|
assert_eq!(window_show_command(false, true), SW_RESTORE);
|
|
// Guard against a regression back to SW_SHOW, which no-ops on a
|
|
// minimized frame and reproduces #4809.
|
|
assert_ne!(window_show_command(false, true), SW_SHOW);
|
|
// Hidden-but-not-minimized restore uses SW_SHOW so a tray-hidden maximized
|
|
// window comes back maximized (#4818), not shrunk to normal size.
|
|
assert_eq!(window_show_command(false, false), SW_SHOW);
|
|
assert_ne!(window_show_command(false, false), SW_RESTORE);
|
|
// Hide path is unchanged regardless of iconic state.
|
|
assert_eq!(window_show_command(true, false), SW_HIDE);
|
|
assert_eq!(window_show_command(true, true), SW_HIDE);
|
|
}
|
|
|
|
/// Test core_rpc_url returns expected format
|
|
#[test]
|
|
fn core_rpc_url_returns_expected_format() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();
|
|
|
|
std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://localhost:9999/rpc");
|
|
let url = core_rpc_url();
|
|
assert_eq!(url, "http://localhost:9999/rpc");
|
|
|
|
std::env::remove_var("OPENHUMAN_CORE_RPC_URL");
|
|
let url = core_rpc_url();
|
|
assert_eq!(url, "http://127.0.0.1:7788/rpc");
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("OPENHUMAN_CORE_RPC_URL", v),
|
|
None => std::env::remove_var("OPENHUMAN_CORE_RPC_URL"),
|
|
}
|
|
}
|
|
|
|
/// Test overlay_parent_rpc_url handles empty env var
|
|
#[test]
|
|
fn overlay_parent_rpc_url_handles_empty() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var("OPENHUMAN_CORE_RPC_URL").ok();
|
|
|
|
std::env::set_var("OPENHUMAN_CORE_RPC_URL", "");
|
|
assert!(overlay_parent_rpc_url().is_none());
|
|
|
|
std::env::set_var("OPENHUMAN_CORE_RPC_URL", " ");
|
|
assert!(overlay_parent_rpc_url().is_none());
|
|
|
|
std::env::set_var("OPENHUMAN_CORE_RPC_URL", "http://127.0.0.1:7788/rpc");
|
|
assert_eq!(
|
|
overlay_parent_rpc_url(),
|
|
Some("http://127.0.0.1:7788/rpc".to_string())
|
|
);
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("OPENHUMAN_CORE_RPC_URL", v),
|
|
None => std::env::remove_var("OPENHUMAN_CORE_RPC_URL"),
|
|
}
|
|
}
|
|
|
|
// NOTE: `setup_tray_function_signature_compiles` and `app_runtime_type_exists`
|
|
// removed (plan.md §2.1) — one had an empty body, the other's only real
|
|
// assertion was commented out; both were compile-only no-ops.
|
|
|
|
#[test]
|
|
fn no_app_update_available_result_is_quiet_unavailable() {
|
|
let info = no_app_update_available("0.53.43".to_string());
|
|
|
|
assert_eq!(info.current_version, "0.53.43");
|
|
assert!(!info.available);
|
|
assert!(info.available_version.is_none());
|
|
assert!(info.body.is_none());
|
|
}
|
|
|
|
// NOTE: `tray_setup_logging_patterns_exist` removed (plan.md §2.1) — a
|
|
// comments-only body that asserted nothing.
|
|
|
|
// -------------------------------------------------------------------------
|
|
// macos_os_version (issue #1012)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/// On macOS, sw_vers is always present and must return a version string.
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_os_version_returns_some() {
|
|
assert!(
|
|
macos_os_version().is_some(),
|
|
"sw_vers -productVersion must succeed on macOS"
|
|
);
|
|
}
|
|
|
|
/// The returned version must be a non-empty trimmed string.
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_os_version_is_nonempty() {
|
|
let ver = macos_os_version().expect("sw_vers must return a version on macOS");
|
|
assert!(!ver.is_empty());
|
|
// No leading/trailing whitespace (the impl trims).
|
|
assert_eq!(ver, ver.trim());
|
|
}
|
|
|
|
/// The version string must look like dot-separated integers ("14.5", "13.2.1").
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_os_version_is_dotted_integer_format() {
|
|
let ver = macos_os_version().expect("sw_vers must return a version on macOS");
|
|
let all_numeric_parts = ver
|
|
.split('.')
|
|
.all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()));
|
|
assert!(
|
|
all_numeric_parts,
|
|
"os version {ver:?} must be dot-separated integers (e.g. '14.5')"
|
|
);
|
|
}
|
|
|
|
/// The version must have at least one component (e.g. a bare major "15" is valid).
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn macos_os_version_has_at_least_one_component() {
|
|
let ver = macos_os_version().expect("sw_vers must return a version on macOS");
|
|
assert!(
|
|
!ver.split('.').next().unwrap_or("").is_empty(),
|
|
"version must have at least one numeric component"
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// WSL + X11 desktop startup warning (issue #1653)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn wsl_x11_warning_detects_classic_x11_forwarding() {
|
|
assert!(should_warn_for_wsl_x11_desktop(true, true, false, false));
|
|
}
|
|
|
|
#[test]
|
|
fn wsl_x11_warning_skips_non_wsl_or_headless_runs() {
|
|
assert!(!should_warn_for_wsl_x11_desktop(false, true, false, false));
|
|
assert!(!should_warn_for_wsl_x11_desktop(true, false, false, false));
|
|
}
|
|
|
|
#[test]
|
|
fn wsl_x11_warning_skips_wslg_or_wayland_runs() {
|
|
assert!(!should_warn_for_wsl_x11_desktop(true, true, true, false));
|
|
assert!(!should_warn_for_wsl_x11_desktop(true, true, false, true));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Linux display-server pre-flight (Sentry OPENHUMAN-TAURI-K1)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn linux_display_present_with_x11() {
|
|
assert!(linux_display_server_present(true, false));
|
|
}
|
|
|
|
#[test]
|
|
fn linux_display_present_with_wayland() {
|
|
assert!(linux_display_server_present(false, true));
|
|
}
|
|
|
|
#[test]
|
|
fn linux_display_present_with_both() {
|
|
assert!(linux_display_server_present(true, true));
|
|
}
|
|
|
|
#[test]
|
|
fn linux_display_absent_without_either() {
|
|
assert!(!linux_display_server_present(false, false));
|
|
}
|
|
|
|
#[test]
|
|
fn linux_root_uid_detected() {
|
|
assert!(linux_is_root_uid(0));
|
|
}
|
|
|
|
#[test]
|
|
fn linux_non_root_uid_not_detected() {
|
|
assert!(!linux_is_root_uid(1000));
|
|
assert!(!linux_is_root_uid(1));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Linux D-Bus session-bus probe (Sentry OPENHUMAN-TAURI-TM)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dbus_address_unix_is_supported() {
|
|
assert!(dbus_address_is_supported("unix:path=/run/user/1000/bus"));
|
|
assert!(dbus_address_is_supported("unix:abstract=/tmp/dbus-abc"));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_address_tcp_and_launchd_supported() {
|
|
assert!(dbus_address_is_supported("tcp:host=localhost,port=1234"));
|
|
assert!(dbus_address_is_supported(
|
|
"launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET"
|
|
));
|
|
assert!(dbus_address_is_supported("autolaunch:"));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_address_disabled_is_unsupported() {
|
|
// The literal value WSL2-without-WSLg sets — root cause of the panic.
|
|
assert!(!dbus_address_is_supported("disabled"));
|
|
assert!(!dbus_address_is_supported(""));
|
|
assert!(!dbus_address_is_supported(" "));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_address_unknown_transport_is_unsupported() {
|
|
assert!(!dbus_address_is_supported("nonce-tcp:host=localhost"));
|
|
assert!(!dbus_address_is_supported("bogus:"));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_address_picks_first_supported_in_list() {
|
|
// zbus walks the semicolon-separated list and uses the first reachable
|
|
// transport, so one good entry is enough.
|
|
assert!(dbus_address_is_supported(
|
|
"disabled;unix:path=/run/user/1000/bus"
|
|
));
|
|
assert!(dbus_address_is_supported(
|
|
"bogus:;tcp:host=localhost,port=55"
|
|
));
|
|
assert!(!dbus_address_is_supported("disabled;bogus:"));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_reachable_when_env_addr_is_supported() {
|
|
assert!(linux_dbus_session_reachable(
|
|
Some("unix:path=/run/user/1000/bus"),
|
|
false,
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_unreachable_when_env_addr_disabled() {
|
|
// Even if the socket exists, an explicit `disabled` value means the
|
|
// session bus is intentionally turned off and zbus will reject it.
|
|
assert!(!linux_dbus_session_reachable(Some("disabled"), true));
|
|
}
|
|
|
|
#[test]
|
|
fn dbus_falls_back_to_runtime_socket_when_env_unset() {
|
|
assert!(linux_dbus_session_reachable(None, true));
|
|
assert!(!linux_dbus_session_reachable(None, false));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Platform constants (issue #1012 Sentry tagging)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/// cpu_arch tag is derived from std::env::consts::ARCH which must be non-empty.
|
|
#[test]
|
|
fn platform_arch_constant_is_nonempty() {
|
|
assert!(
|
|
!std::env::consts::ARCH.is_empty(),
|
|
"ARCH constant used for Sentry cpu_arch tag must be non-empty"
|
|
);
|
|
}
|
|
|
|
/// os_name tag is derived from std::env::consts::OS which must be non-empty.
|
|
#[test]
|
|
fn platform_os_constant_is_nonempty() {
|
|
assert!(
|
|
!std::env::consts::OS.is_empty(),
|
|
"OS constant used for Sentry os_name tag must be non-empty"
|
|
);
|
|
}
|
|
|
|
/// On a macOS build the OS constant must equal "macos".
|
|
#[cfg(target_os = "macos")]
|
|
#[test]
|
|
fn platform_os_is_macos_on_macos_build() {
|
|
assert_eq!(std::env::consts::OS, "macos");
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_force_swiftshader_on_linux() {
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "linux", "x86_64", None, None);
|
|
|
|
// #4193: the GPU process must NOT be killed outright — `--disable-gpu`
|
|
// takes every WebGL surface (the Tiny Place world renderer) down with it.
|
|
assert!(
|
|
!args.contains(&("--disable-gpu", None)),
|
|
"--disable-gpu kills WebGL and must not be set, got: {args:?}"
|
|
);
|
|
// Instead the GPU process is pinned to ANGLE/SwiftShader software GL, which
|
|
// keeps WebGL available while still avoiding the #1697 hardware-EGL abort.
|
|
assert!(args.contains(&("--use-gl", Some("angle"))));
|
|
assert!(args.contains(&("--use-angle", Some("swiftshader"))));
|
|
assert!(args.contains(&("--enable-unsafe-swiftshader", None)));
|
|
// Page compositing stays on the CPU exactly as before.
|
|
assert!(args.contains(&("--disable-gpu-compositing", None)));
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_disable_intel_macos_compositing_only() {
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "macos", "x86_64", None, None);
|
|
|
|
assert_eq!(args, vec![("--disable-gpu-compositing", None)]);
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_leave_other_platforms_alone() {
|
|
for (os, arch) in [("macos", "aarch64"), ("windows", "x86_64")] {
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, os, arch, None, None);
|
|
|
|
assert!(
|
|
args.is_empty(),
|
|
"unexpected CEF GPU flags for {os}/{arch}: {args:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// OPENHUMAN_FORCE_GPU override (re-enables WebGL2 surfaces — Rive mascot)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn force_gpu_default_off_when_env_unset() {
|
|
assert!(!cef_force_gpu_enabled(None));
|
|
}
|
|
|
|
#[test]
|
|
fn force_gpu_explicit_enable_values_match_prewarm_pattern() {
|
|
for v in ["1", "true", "yes", "on", "TRUE", "Yes", "On"] {
|
|
assert!(
|
|
cef_force_gpu_enabled(Some(v)),
|
|
"OPENHUMAN_FORCE_GPU={v:?} should opt in"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn force_gpu_anything_else_is_off() {
|
|
// Mirrors prewarm semantics: only explicit truthy values opt in.
|
|
for v in ["", "0", "false", "no", "off", "FALSE", "Off", "maybe", " "] {
|
|
assert!(
|
|
!cef_force_gpu_enabled(Some(v)),
|
|
"OPENHUMAN_FORCE_GPU={v:?} must not silently opt in"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_skip_linux_disable_when_force_gpu_set() {
|
|
// Assert the two GPU-disable flags are absent rather than the whole
|
|
// arg list being empty: on root-Linux runners (CI in some configs)
|
|
// the function still appends `--no-sandbox` via the orthogonal
|
|
// OPENHUMAN-TAURI-K1 branch, which would make a strict `is_empty()`
|
|
// check fail spuriously. We only care about the GPU branch here.
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "linux", "x86_64", Some("1"), None);
|
|
|
|
assert!(
|
|
!args.contains(&("--disable-gpu", None)),
|
|
"OPENHUMAN_FORCE_GPU=1 must suppress --disable-gpu, got: {args:?}"
|
|
);
|
|
assert!(
|
|
!args.contains(&("--disable-gpu-compositing", None)),
|
|
"OPENHUMAN_FORCE_GPU=1 must suppress --disable-gpu-compositing, got: {args:?}"
|
|
);
|
|
// With hardware acceleration opted in, the SwiftShader software-GL fallback
|
|
// must NOT be forced either — otherwise WebGL would still be stuck on the
|
|
// software rasteriser despite the override.
|
|
assert!(
|
|
!args.contains(&("--use-angle", Some("swiftshader"))),
|
|
"OPENHUMAN_FORCE_GPU=1 must not force SwiftShader, got: {args:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_force_gpu_does_not_affect_intel_macos_path() {
|
|
// OPENHUMAN_FORCE_GPU only governs the Linux #1697 workaround; the
|
|
// separate Intel-macOS #1012 disable must still apply, regardless of
|
|
// the env var.
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "macos", "x86_64", Some("1"), None);
|
|
|
|
assert_eq!(args, vec![("--disable-gpu-compositing", None)]);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// OPENHUMAN_DISABLE_GPU override (emergency CEF startup escape hatch)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn disable_gpu_default_off_when_env_unset() {
|
|
assert!(!cef_disable_gpu_enabled(None));
|
|
}
|
|
|
|
#[test]
|
|
fn disable_gpu_explicit_enable_values_match_force_gpu_pattern() {
|
|
for v in ["1", "true", "yes", "on", "TRUE", "Yes", "On"] {
|
|
assert!(
|
|
cef_disable_gpu_enabled(Some(v)),
|
|
"OPENHUMAN_DISABLE_GPU={v:?} should opt in"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn disable_gpu_anything_else_is_off() {
|
|
for v in ["", "0", "false", "no", "off", "FALSE", "Off", "maybe", " "] {
|
|
assert!(
|
|
!cef_disable_gpu_enabled(Some(v)),
|
|
"OPENHUMAN_DISABLE_GPU={v:?} must not silently opt in"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_disable_windows_gpu_when_requested() {
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "windows", "x86_64", None, Some("1"));
|
|
|
|
// #4385: on Windows the escape hatch pins CEF to the pure-software
|
|
// ANGLE/SwiftShader backend instead of bare `--disable-gpu`, which is
|
|
// insufficient on NVIDIA Blackwell / RTX 50-series stacks (cef::initialize
|
|
// still returns 0). This mirrors the proven Linux #1697/#4193 fallback.
|
|
assert_eq!(
|
|
args,
|
|
vec![
|
|
("--use-gl", Some("angle")),
|
|
("--use-angle", Some("swiftshader")),
|
|
("--enable-unsafe-swiftshader", None),
|
|
("--disable-gpu-compositing", None),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_windows_disable_gpu_avoids_bare_disable_gpu() {
|
|
// Regression guard for #4385: `--disable-gpu` alone leaves CEF without a
|
|
// working software GL path on unsupported (Blackwell) GPUs and init keeps
|
|
// failing. The Windows escape hatch must route through SwiftShader and must
|
|
// NOT emit the lethal `--disable-gpu` switch.
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "windows", "x86_64", None, Some("1"));
|
|
|
|
assert!(
|
|
!args.contains(&("--disable-gpu", None)),
|
|
"Windows OPENHUMAN_DISABLE_GPU must not emit bare --disable-gpu (#4385), got: {args:?}"
|
|
);
|
|
assert!(
|
|
args.contains(&("--use-angle", Some("swiftshader"))),
|
|
"Windows OPENHUMAN_DISABLE_GPU must force SwiftShader software GL (#4385), got: {args:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn platform_cef_gpu_workarounds_disable_gpu_wins_over_linux_force_gpu() {
|
|
let mut args = Vec::new();
|
|
append_platform_cef_gpu_workarounds(&mut args, "linux", "x86_64", Some("1"), Some("1"));
|
|
|
|
assert!(args.contains(&("--disable-gpu", None)));
|
|
assert!(args.contains(&("--disable-gpu-compositing", None)));
|
|
assert!(
|
|
!args.contains(&("--use-angle", Some("swiftshader"))),
|
|
"OPENHUMAN_DISABLE_GPU=1 must not also force SwiftShader, got: {args:?}"
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// #3554 — never forward --time-ticks-at-unix-epoch to CEF (wrong system time)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn time_ticks_flag_matches_any_dash_and_casing_form() {
|
|
for flag in [
|
|
"--time-ticks-at-unix-epoch",
|
|
"-time-ticks-at-unix-epoch",
|
|
"time-ticks-at-unix-epoch",
|
|
"--Time-Ticks-At-Unix-Epoch",
|
|
] {
|
|
assert!(
|
|
is_time_ticks_at_unix_epoch_flag(flag),
|
|
"{flag:?} should be recognised as the time-ticks switch"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn time_ticks_flag_does_not_match_unrelated_flags() {
|
|
for flag in [
|
|
"--disable-gpu",
|
|
"--use-mock-keychain",
|
|
"--time-zone",
|
|
"--enable-features",
|
|
] {
|
|
assert!(
|
|
!is_time_ticks_at_unix_epoch_flag(flag),
|
|
"{flag:?} must not be treated as the time-ticks switch"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn strip_time_ticks_removes_negative_value_and_keeps_the_rest() {
|
|
// The corrupt value reported in #3554, alongside flags we must preserve.
|
|
let mut args = vec![
|
|
("--use-mock-keychain", None),
|
|
("--time-ticks-at-unix-epoch", Some("-1780937467390432")),
|
|
("--disable-gpu", None),
|
|
];
|
|
strip_time_ticks_at_unix_epoch(&mut args);
|
|
|
|
assert_eq!(
|
|
args,
|
|
vec![("--use-mock-keychain", None), ("--disable-gpu", None)],
|
|
"the corrupt time-ticks switch must be removed; everything else kept"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_time_ticks_removes_inline_value_form() {
|
|
// The critical bypass case: when the value is carried inline as
|
|
// `--flag=value` (a single token) rather than as a separate value, the
|
|
// matcher must still recognise and strip it.
|
|
let mut args = vec![
|
|
("--use-mock-keychain", None),
|
|
("--time-ticks-at-unix-epoch=-1780937467390432", None),
|
|
("--disable-gpu", None),
|
|
];
|
|
strip_time_ticks_at_unix_epoch(&mut args);
|
|
|
|
assert_eq!(
|
|
args,
|
|
vec![("--use-mock-keychain", None), ("--disable-gpu", None)],
|
|
"the inline time-ticks switch must be removed; everything else kept"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_time_ticks_removes_the_flag_regardless_of_value() {
|
|
// Even a non-negative value is dropped: OpenHuman must let Chromium
|
|
// compute the clock origin rather than anchor it from the shell.
|
|
let mut args = vec![
|
|
("--time-ticks-at-unix-epoch", Some("1780937467390432")),
|
|
("--time-ticks-at-unix-epoch", None),
|
|
];
|
|
strip_time_ticks_at_unix_epoch(&mut args);
|
|
|
|
assert!(
|
|
args.is_empty(),
|
|
"all time-ticks variants must be stripped, got: {args:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn strip_time_ticks_is_a_noop_without_the_flag() {
|
|
let mut args = vec![("--use-mock-keychain", None), ("--disable-gpu", None)];
|
|
let expected = args.clone();
|
|
strip_time_ticks_at_unix_epoch(&mut args);
|
|
|
|
assert_eq!(args, expected, "unrelated args must be left untouched");
|
|
}
|
|
|
|
/// On an Intel macOS build the ARCH constant must equal "x86_64".
|
|
/// This is the architecture that triggers --disable-gpu-compositing.
|
|
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
|
#[test]
|
|
fn platform_arch_is_x86_64_on_intel_build() {
|
|
assert_eq!(std::env::consts::ARCH, "x86_64");
|
|
}
|
|
|
|
/// On Apple Silicon the ARCH constant must equal "aarch64"; the GPU flag
|
|
/// must NOT be compiled in (verified by this test existing in the binary).
|
|
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
|
#[test]
|
|
fn platform_arch_is_aarch64_on_apple_silicon_build() {
|
|
assert_eq!(std::env::consts::ARCH, "aarch64");
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// cef_prewarm_enabled (issue #2463 — Wayland/XWayland BadWindow guard)
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prewarm_enabled_by_default_on_non_wayland() {
|
|
assert!(cef_prewarm_enabled(None, false));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_auto_disabled_on_wayland_when_env_unset() {
|
|
assert!(!cef_prewarm_enabled(None, true));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_explicit_disable_respected_on_non_wayland() {
|
|
assert!(!cef_prewarm_enabled(Some("0"), false));
|
|
assert!(!cef_prewarm_enabled(Some("false"), false));
|
|
assert!(!cef_prewarm_enabled(Some("no"), false));
|
|
assert!(!cef_prewarm_enabled(Some("off"), false));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_explicit_disable_respected_on_wayland() {
|
|
assert!(!cef_prewarm_enabled(Some("0"), true));
|
|
assert!(!cef_prewarm_enabled(Some("false"), true));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_explicit_enable_overrides_wayland_guard() {
|
|
// OPENHUMAN_CEF_PREWARM=1 (or any non-disable value) lets ops
|
|
// force prewarm even on Wayland sessions.
|
|
assert!(cef_prewarm_enabled(Some("1"), true));
|
|
assert!(cef_prewarm_enabled(Some("true"), true));
|
|
assert!(cef_prewarm_enabled(Some("yes"), true));
|
|
assert!(cef_prewarm_enabled(Some("on"), true));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_disable_flags_are_case_insensitive() {
|
|
assert!(!cef_prewarm_enabled(Some("FALSE"), false));
|
|
assert!(!cef_prewarm_enabled(Some("OFF"), true));
|
|
assert!(!cef_prewarm_enabled(Some(" 0 "), false));
|
|
assert!(!cef_prewarm_enabled(Some(" No "), true));
|
|
}
|
|
|
|
#[test]
|
|
fn prewarm_unknown_env_value_treated_as_enable() {
|
|
// Any string that is not a recognised disable token → treat as enable.
|
|
assert!(cef_prewarm_enabled(Some("enabled"), false));
|
|
assert!(cef_prewarm_enabled(Some("yes"), false));
|
|
assert!(cef_prewarm_enabled(Some(""), false));
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// build_sentry_release_tag
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn sentry_release_tag_starts_with_openhuman() {
|
|
let tag = build_sentry_release_tag();
|
|
assert!(
|
|
tag.starts_with("openhuman@"),
|
|
"release tag must start with 'openhuman@', got: {tag:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sentry_release_tag_contains_cargo_pkg_version() {
|
|
let tag = build_sentry_release_tag();
|
|
let version = env!("CARGO_PKG_VERSION");
|
|
assert!(
|
|
tag.contains(version),
|
|
"release tag {tag:?} must embed CARGO_PKG_VERSION {version:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sentry_release_tag_version_part_is_nonempty() {
|
|
let tag = build_sentry_release_tag();
|
|
let after_prefix = tag.strip_prefix("openhuman@").unwrap_or("");
|
|
assert!(!after_prefix.is_empty(), "version part must not be empty");
|
|
}
|
|
|
|
/// When a SHA is baked in the tag takes the form `openhuman@<ver>+<sha12>`.
|
|
/// When it is not, the tag is simply `openhuman@<ver>` with no `+`.
|
|
/// Either way the full tag must be non-empty.
|
|
#[test]
|
|
fn sentry_release_tag_is_nonempty() {
|
|
assert!(!build_sentry_release_tag().is_empty());
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// resolve_sentry_environment
|
|
// -------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn sentry_environment_reads_openhuman_app_env() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let key = "OPENHUMAN_APP_ENV";
|
|
let original = std::env::var(key).ok();
|
|
std::env::set_var(key, "staging");
|
|
let env = resolve_sentry_environment();
|
|
match original {
|
|
Some(v) => std::env::set_var(key, v),
|
|
None => std::env::remove_var(key),
|
|
}
|
|
assert_eq!(env, "staging");
|
|
}
|
|
|
|
#[test]
|
|
fn sentry_environment_trims_whitespace_from_openhuman_app_env() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let key = "OPENHUMAN_APP_ENV";
|
|
let original = std::env::var(key).ok();
|
|
std::env::set_var(key, " dev ");
|
|
let env = resolve_sentry_environment();
|
|
match original {
|
|
Some(v) => std::env::set_var(key, v),
|
|
None => std::env::remove_var(key),
|
|
}
|
|
assert_eq!(env, "dev");
|
|
}
|
|
|
|
#[test]
|
|
fn sentry_environment_skips_empty_openhuman_app_env() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let key = "OPENHUMAN_APP_ENV";
|
|
let original = std::env::var(key).ok();
|
|
std::env::set_var(key, "");
|
|
let env = resolve_sentry_environment();
|
|
match original {
|
|
Some(v) => std::env::set_var(key, v),
|
|
None => std::env::remove_var(key),
|
|
}
|
|
// Falls through to VITE_ compile-time value or "production"; must be non-empty.
|
|
assert!(!env.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn sentry_environment_skips_whitespace_only_openhuman_app_env() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let key = "OPENHUMAN_APP_ENV";
|
|
let original = std::env::var(key).ok();
|
|
std::env::set_var(key, " ");
|
|
let env = resolve_sentry_environment();
|
|
match original {
|
|
Some(v) => std::env::set_var(key, v),
|
|
None => std::env::remove_var(key),
|
|
}
|
|
assert!(!env.is_empty());
|
|
}
|
|
|
|
/// When neither runtime env var nor compile-time VITE_ is set, the fallback
|
|
/// must be "production". Guard with a compile-time check so this test only
|
|
/// asserts the hard default when no compile-time override is present.
|
|
#[test]
|
|
fn sentry_environment_defaults_to_production_when_unset() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
if option_env!("VITE_OPENHUMAN_APP_ENV").is_some() {
|
|
// A compile-time override is baked in; skip — the fallback path is
|
|
// exercised by sentry_environment_skips_empty_openhuman_app_env.
|
|
return;
|
|
}
|
|
let key = "OPENHUMAN_APP_ENV";
|
|
let original = std::env::var(key).ok();
|
|
std::env::remove_var(key);
|
|
let env = resolve_sentry_environment();
|
|
match original {
|
|
Some(v) => std::env::set_var(key, v),
|
|
None => std::env::remove_var(key),
|
|
}
|
|
assert_eq!(env, "production");
|
|
}
|
|
|
|
// ── Sentry before_send filter: drop "Failed to request http://localhost:…"
|
|
// noise emitted by the vendored tauri-runtime-cef dev proxy in packaged
|
|
// builds (issue OPENHUMAN-TAURI-V). Tests target the pure
|
|
// `message_is_localhost_dev_fetch_noise` helper so the rule can be
|
|
// asserted without standing up a Sentry client.
|
|
|
|
#[test]
|
|
fn localhost_dev_fetch_noise_drops_vite_dev_url_1420() {
|
|
// The exact message shape reported by the latest event tag in Sentry
|
|
// (URL repeated by reqwest's `error sending request for url (…)`).
|
|
let msg = "Failed to request http://localhost:1420/components/skills/SkillCard.tsx: \
|
|
error sending request for url (http://localhost:1420/components/skills/SkillCard.tsx)";
|
|
assert!(
|
|
message_is_localhost_dev_fetch_noise(msg),
|
|
"expected Vite dev-server fetch failure to be filtered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn localhost_dev_fetch_noise_drops_127_0_0_1_dev_url() {
|
|
// Some environments resolve `localhost` to 127.0.0.1 at the reqwest
|
|
// layer; the formatted message can carry either spelling.
|
|
let msg = "Failed to request http://127.0.0.1:1420/index.html: \
|
|
error sending request for url (http://127.0.0.1:1420/index.html)";
|
|
assert!(
|
|
message_is_localhost_dev_fetch_noise(msg),
|
|
"expected 127.0.0.1 dev-server fetch failure to be filtered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn localhost_dev_fetch_noise_passes_production_url_through() {
|
|
// Real upstream failures (e.g. backend API errors surfaced via the
|
|
// same `Failed to request …` wording elsewhere) must NOT be filtered —
|
|
// they're the high-signal events Sentry exists for.
|
|
let msg = "Failed to request https://api.openhuman.ai/v1/skills: \
|
|
error sending request for url (https://api.openhuman.ai/v1/skills)";
|
|
assert!(
|
|
!message_is_localhost_dev_fetch_noise(msg),
|
|
"production API errors must NOT be filtered out"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn localhost_dev_fetch_noise_passes_unrelated_localhost_messages() {
|
|
// The filter is anchored on the dev-proxy's exact prefix to avoid
|
|
// accidentally dropping any error that happens to mention localhost
|
|
// (e.g. core-sidecar transport errors logged from coreRpcClient).
|
|
let msg =
|
|
"[core_rpc] transport error: error sending request for url (http://localhost:7788/rpc)";
|
|
assert!(
|
|
!message_is_localhost_dev_fetch_noise(msg),
|
|
"non-tauri-cef localhost errors must NOT be filtered"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn event_filter_uses_message_field() {
|
|
// event-level coverage: when sentry-tracing populates
|
|
// `event.message` (default with `attach_stacktrace=false`), the
|
|
// filter should see the noise payload through the primary read
|
|
// path. Per graycyrus on PR #1545.
|
|
let mut event = sentry::protocol::Event::new();
|
|
event.message = Some("Failed to request http://localhost:1420/foo: timeout".into());
|
|
assert!(
|
|
event_is_localhost_dev_fetch_noise(&event),
|
|
"event.message read path must catch noise messages"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn event_filter_falls_back_to_last_exception_value() {
|
|
// event-level coverage: if `attach_stacktrace` is ever turned on,
|
|
// sentry-tracing populates `event.exception` instead of (or in
|
|
// addition to) `event.message`. Filter must still see the noise
|
|
// payload through the exception fallback. Per graycyrus on PR #1545.
|
|
let mut event = sentry::protocol::Event::new();
|
|
event.message = None;
|
|
event.exception.values.push(sentry::protocol::Exception {
|
|
ty: "log".into(),
|
|
value: Some("Failed to request http://localhost:1420/foo: timeout".into()),
|
|
..Default::default()
|
|
});
|
|
assert!(
|
|
event_is_localhost_dev_fetch_noise(&event),
|
|
"exception fallback must catch noise messages when event.message is absent"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn event_filter_passes_through_when_neither_field_matches() {
|
|
// Negative event-level case: no noise prefix in either field →
|
|
// event must NOT be filtered.
|
|
let mut event = sentry::protocol::Event::new();
|
|
event.message = Some("genuine production error".into());
|
|
event.exception.values.push(sentry::protocol::Exception {
|
|
ty: "log".into(),
|
|
value: Some("connection refused (10061)".into()),
|
|
..Default::default()
|
|
});
|
|
assert!(
|
|
!event_is_localhost_dev_fetch_noise(&event),
|
|
"legitimate production events must pass through"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn localhost_dev_fetch_noise_anchors_to_message_start() {
|
|
// CodeRabbit (PR #1545) caught that the predicate used
|
|
// `contains` rather than `starts_with`. Regression: a message
|
|
// that merely embeds the dev-proxy prefix later in its text
|
|
// must NOT be filtered — only messages that *begin* with it.
|
|
let msg = "User report: `Failed to request http://localhost:1420/foo` was logged earlier";
|
|
assert!(
|
|
!message_is_localhost_dev_fetch_noise(msg),
|
|
"messages that merely contain the dev-proxy prefix must NOT be filtered"
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// path_has_executable / deep-link xdg-mime pre-flight (OPENHUMAN-TAURI-AS)
|
|
// -------------------------------------------------------------------------
|
|
|
|
/// With a controlled `$PATH` containing one dir that holds a file named
|
|
/// `xdg-mime`, the lookup must succeed (mirrors a Linux desktop install
|
|
/// where xdg-utils ships the binary).
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn path_has_executable_finds_file_on_path() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var_os("PATH");
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::write(dir.path().join("xdg-mime"), b"#!/bin/sh\n").expect("write stub");
|
|
std::env::set_var("PATH", dir.path());
|
|
|
|
assert!(
|
|
path_has_executable("xdg-mime"),
|
|
"must discover xdg-mime when present in a $PATH entry"
|
|
);
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("PATH", v),
|
|
None => std::env::remove_var("PATH"),
|
|
}
|
|
}
|
|
|
|
/// With a controlled `$PATH` that does NOT contain `xdg-mime`, the lookup
|
|
/// must fail (mirrors WSL2 / minimal containers without xdg-utils — the
|
|
/// case OPENHUMAN-TAURI-AS protects against).
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn path_has_executable_returns_false_when_missing() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var_os("PATH");
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
// Intentionally do not create xdg-mime in `dir`.
|
|
std::env::set_var("PATH", dir.path());
|
|
|
|
assert!(
|
|
!path_has_executable("xdg-mime"),
|
|
"must return false when xdg-mime is not in any $PATH entry"
|
|
);
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("PATH", v),
|
|
None => std::env::remove_var("PATH"),
|
|
}
|
|
}
|
|
|
|
/// When `$PATH` is unset entirely, the lookup must short-circuit to false
|
|
/// rather than panic or fall back to the cwd.
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn path_has_executable_returns_false_when_path_unset() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var_os("PATH");
|
|
|
|
std::env::remove_var("PATH");
|
|
assert!(
|
|
!path_has_executable("xdg-mime"),
|
|
"unset $PATH must yield false (skip register_all on the missing-xdg-utils branch)"
|
|
);
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("PATH", v),
|
|
None => std::env::remove_var("PATH"),
|
|
}
|
|
}
|
|
|
|
/// Regression guard for OPENHUMAN-TAURI-5V: a Linux host with `xdg-mime`
|
|
/// installed but `update-desktop-database` missing must classify as
|
|
/// "skip register_all" — the pre-#5V code only checked `xdg-mime` and
|
|
/// would have entered the plugin call, which then fires the noisy
|
|
/// `Failed to run OS command \`update-desktop-database\`` internal log
|
|
/// that escapes to Sentry. The Wave-4 fix pre-flights every xdg-utils
|
|
/// binary the plugin shells out to; this test pins that contract by
|
|
/// checking each binary lookup independently with a `$PATH` that
|
|
/// contains only `xdg-mime`.
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn path_has_executable_returns_false_for_partial_xdg_utils_install() {
|
|
let _g = ENV_LOCK.lock().unwrap();
|
|
let original = std::env::var_os("PATH");
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
// Only `xdg-mime` exists; `update-desktop-database` and
|
|
// `xdg-icon-resource` are deliberately absent.
|
|
std::fs::write(dir.path().join("xdg-mime"), b"#!/bin/sh\n").expect("write stub");
|
|
std::env::set_var("PATH", dir.path());
|
|
|
|
assert!(
|
|
path_has_executable("xdg-mime"),
|
|
"xdg-mime stub must be discoverable in the partial-install $PATH"
|
|
);
|
|
assert!(
|
|
!path_has_executable("update-desktop-database"),
|
|
"partial xdg-utils install must NOT report update-desktop-database present (OPENHUMAN-TAURI-5V)"
|
|
);
|
|
assert!(
|
|
!path_has_executable("xdg-icon-resource"),
|
|
"partial xdg-utils install must NOT report xdg-icon-resource present"
|
|
);
|
|
|
|
match original {
|
|
Some(v) => std::env::set_var("PATH", v),
|
|
None => std::env::remove_var("PATH"),
|
|
}
|
|
}
|
|
|
|
/// Regression guard for issue #2228: `tauri-plugin-single-instance` must
|
|
/// enable the `deep-link` feature so that second-launch deep-link payloads
|
|
/// (e.g. `openhuman://oauth/...` callbacks from Windows/Linux system
|
|
/// browsers) are forwarded into the primary instance. Without it, hot OAuth
|
|
/// callbacks silently no-op while only focusing the existing window.
|
|
#[test]
|
|
fn single_instance_dep_enables_deep_link_feature() {
|
|
let manifest_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
|
|
let manifest = std::fs::read_to_string(&manifest_path).expect("read app/src-tauri/Cargo.toml");
|
|
let parsed: toml::Value = manifest.parse().expect("parse Cargo.toml");
|
|
|
|
let dep = parsed
|
|
.get("dependencies")
|
|
.and_then(|d| d.get("tauri-plugin-single-instance"))
|
|
.expect("tauri-plugin-single-instance dependency must exist");
|
|
|
|
let features = dep.get("features").and_then(|f| f.as_array()).expect(
|
|
"tauri-plugin-single-instance must be a table with a `features` array \
|
|
— issue #2228 requires the `deep-link` feature to forward hot-instance \
|
|
OAuth callbacks on Windows/Linux",
|
|
);
|
|
|
|
assert!(
|
|
features.iter().any(|v| v.as_str() == Some("deep-link")),
|
|
"tauri-plugin-single-instance must enable the `deep-link` feature \
|
|
(issue #2228 — hot-instance OAuth callback forwarding)"
|
|
);
|
|
}
|