@@ -1881,6 +1881,12 @@ fn macos_os_version() -> Option<String> {
18811881mod tests {
18821882 use super :: * ;
18831883
1884+ // Tests that read/write process-global env vars must serialize through this
1885+ // mutex. Rust's test runner executes tests in parallel by default; without
1886+ // coordination, concurrent set_var / remove_var calls race and produce
1887+ // spurious failures.
1888+ static ENV_LOCK : std:: sync:: Mutex < ( ) > = std:: sync:: Mutex :: new ( ( ) ) ;
1889+
18841890 /// Test that is_daemon_mode correctly detects daemon flag variations
18851891 #[ test]
18861892 fn is_daemon_mode_detects_daemon_flag ( ) {
@@ -1892,20 +1898,17 @@ mod tests {
18921898 /// Test core_rpc_url returns expected format
18931899 #[ test]
18941900 fn core_rpc_url_returns_expected_format ( ) {
1895- // Save original env
1901+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
18961902 let original = std:: env:: var ( "OPENHUMAN_CORE_RPC_URL" ) . ok ( ) ;
18971903
1898- // Test with env var set
18991904 std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , "http://localhost:9999/rpc" ) ;
19001905 let url = core_rpc_url ( ) ;
19011906 assert_eq ! ( url, "http://localhost:9999/rpc" ) ;
19021907
1903- // Test fallback when env not set
19041908 std:: env:: remove_var ( "OPENHUMAN_CORE_RPC_URL" ) ;
19051909 let url = core_rpc_url ( ) ;
19061910 assert_eq ! ( url, "http://127.0.0.1:7788/rpc" ) ;
19071911
1908- // Restore original
19091912 match original {
19101913 Some ( v) => std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , v) ,
19111914 None => std:: env:: remove_var ( "OPENHUMAN_CORE_RPC_URL" ) ,
@@ -1915,25 +1918,21 @@ mod tests {
19151918 /// Test overlay_parent_rpc_url handles empty env var
19161919 #[ test]
19171920 fn overlay_parent_rpc_url_handles_empty ( ) {
1918- // Save original env
1921+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
19191922 let original = std:: env:: var ( "OPENHUMAN_CORE_RPC_URL" ) . ok ( ) ;
19201923
1921- // Test with empty string (should return None)
19221924 std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , "" ) ;
1923- let result = overlay_parent_rpc_url ( ) ;
1924- assert ! ( result. is_none( ) ) ;
1925+ assert ! ( overlay_parent_rpc_url( ) . is_none( ) ) ;
19251926
1926- // Test with whitespace only (should return None)
19271927 std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , " " ) ;
1928- let result = overlay_parent_rpc_url ( ) ;
1929- assert ! ( result. is_none( ) ) ;
1928+ assert ! ( overlay_parent_rpc_url( ) . is_none( ) ) ;
19301929
1931- // Test with valid URL
19321930 std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , "http://127.0.0.1:7788/rpc" ) ;
1933- let result = overlay_parent_rpc_url ( ) ;
1934- assert_eq ! ( result, Some ( "http://127.0.0.1:7788/rpc" . to_string( ) ) ) ;
1931+ assert_eq ! (
1932+ overlay_parent_rpc_url( ) ,
1933+ Some ( "http://127.0.0.1:7788/rpc" . to_string( ) )
1934+ ) ;
19351935
1936- // Restore original
19371936 match original {
19381937 Some ( v) => std:: env:: set_var ( "OPENHUMAN_CORE_RPC_URL" , v) ,
19391938 None => std:: env:: remove_var ( "OPENHUMAN_CORE_RPC_URL" ) ,
@@ -1983,4 +1982,219 @@ mod tests {
19831982 //
19841983 // This test passes if the code compiles with these log messages.
19851984 }
1985+
1986+ // -------------------------------------------------------------------------
1987+ // macos_os_version (issue #1012)
1988+ // -------------------------------------------------------------------------
1989+
1990+ /// On macOS, sw_vers is always present and must return a version string.
1991+ #[ cfg( target_os = "macos" ) ]
1992+ #[ test]
1993+ fn macos_os_version_returns_some ( ) {
1994+ assert ! (
1995+ macos_os_version( ) . is_some( ) ,
1996+ "sw_vers -productVersion must succeed on macOS"
1997+ ) ;
1998+ }
1999+
2000+ /// The returned version must be a non-empty trimmed string.
2001+ #[ cfg( target_os = "macos" ) ]
2002+ #[ test]
2003+ fn macos_os_version_is_nonempty ( ) {
2004+ let ver = macos_os_version ( ) . expect ( "sw_vers must return a version on macOS" ) ;
2005+ assert ! ( !ver. is_empty( ) ) ;
2006+ // No leading/trailing whitespace (the impl trims).
2007+ assert_eq ! ( ver, ver. trim( ) ) ;
2008+ }
2009+
2010+ /// The version string must look like dot-separated integers ("14.5", "13.2.1").
2011+ #[ cfg( target_os = "macos" ) ]
2012+ #[ test]
2013+ fn macos_os_version_is_dotted_integer_format ( ) {
2014+ let ver = macos_os_version ( ) . expect ( "sw_vers must return a version on macOS" ) ;
2015+ let all_numeric_parts = ver
2016+ . split ( '.' )
2017+ . all ( |part| !part. is_empty ( ) && part. chars ( ) . all ( |c| c. is_ascii_digit ( ) ) ) ;
2018+ assert ! (
2019+ all_numeric_parts,
2020+ "os version {ver:?} must be dot-separated integers (e.g. '14.5')"
2021+ ) ;
2022+ }
2023+
2024+ /// The version must have at least one component (e.g. a bare major "15" is valid).
2025+ #[ cfg( target_os = "macos" ) ]
2026+ #[ test]
2027+ fn macos_os_version_has_at_least_one_component ( ) {
2028+ let ver = macos_os_version ( ) . expect ( "sw_vers must return a version on macOS" ) ;
2029+ assert ! (
2030+ !ver. split( '.' ) . next( ) . unwrap_or( "" ) . is_empty( ) ,
2031+ "version must have at least one numeric component"
2032+ ) ;
2033+ }
2034+
2035+ // -------------------------------------------------------------------------
2036+ // Platform constants (issue #1012 Sentry tagging)
2037+ // -------------------------------------------------------------------------
2038+
2039+ /// cpu_arch tag is derived from std::env::consts::ARCH which must be non-empty.
2040+ #[ test]
2041+ fn platform_arch_constant_is_nonempty ( ) {
2042+ assert ! (
2043+ !std:: env:: consts:: ARCH . is_empty( ) ,
2044+ "ARCH constant used for Sentry cpu_arch tag must be non-empty"
2045+ ) ;
2046+ }
2047+
2048+ /// os_name tag is derived from std::env::consts::OS which must be non-empty.
2049+ #[ test]
2050+ fn platform_os_constant_is_nonempty ( ) {
2051+ assert ! (
2052+ !std:: env:: consts:: OS . is_empty( ) ,
2053+ "OS constant used for Sentry os_name tag must be non-empty"
2054+ ) ;
2055+ }
2056+
2057+ /// On a macOS build the OS constant must equal "macos".
2058+ #[ cfg( target_os = "macos" ) ]
2059+ #[ test]
2060+ fn platform_os_is_macos_on_macos_build ( ) {
2061+ assert_eq ! ( std:: env:: consts:: OS , "macos" ) ;
2062+ }
2063+
2064+ /// On an Intel macOS build the ARCH constant must equal "x86_64".
2065+ /// This is the architecture that triggers --disable-gpu-compositing.
2066+ #[ cfg( all( target_os = "macos" , target_arch = "x86_64" ) ) ]
2067+ #[ test]
2068+ fn platform_arch_is_x86_64_on_intel_build ( ) {
2069+ assert_eq ! ( std:: env:: consts:: ARCH , "x86_64" ) ;
2070+ }
2071+
2072+ /// On Apple Silicon the ARCH constant must equal "aarch64"; the GPU flag
2073+ /// must NOT be compiled in (verified by this test existing in the binary).
2074+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
2075+ #[ test]
2076+ fn platform_arch_is_aarch64_on_apple_silicon_build ( ) {
2077+ assert_eq ! ( std:: env:: consts:: ARCH , "aarch64" ) ;
2078+ }
2079+
2080+ // -------------------------------------------------------------------------
2081+ // build_sentry_release_tag
2082+ // -------------------------------------------------------------------------
2083+
2084+ #[ test]
2085+ fn sentry_release_tag_starts_with_openhuman ( ) {
2086+ let tag = build_sentry_release_tag ( ) ;
2087+ assert ! (
2088+ tag. starts_with( "openhuman@" ) ,
2089+ "release tag must start with 'openhuman@', got: {tag:?}"
2090+ ) ;
2091+ }
2092+
2093+ #[ test]
2094+ fn sentry_release_tag_contains_cargo_pkg_version ( ) {
2095+ let tag = build_sentry_release_tag ( ) ;
2096+ let version = env ! ( "CARGO_PKG_VERSION" ) ;
2097+ assert ! (
2098+ tag. contains( version) ,
2099+ "release tag {tag:?} must embed CARGO_PKG_VERSION {version:?}"
2100+ ) ;
2101+ }
2102+
2103+ #[ test]
2104+ fn sentry_release_tag_version_part_is_nonempty ( ) {
2105+ let tag = build_sentry_release_tag ( ) ;
2106+ let after_prefix = tag. strip_prefix ( "openhuman@" ) . unwrap_or ( "" ) ;
2107+ assert ! ( !after_prefix. is_empty( ) , "version part must not be empty" ) ;
2108+ }
2109+
2110+ /// When a SHA is baked in the tag takes the form `openhuman@<ver>+<sha12>`.
2111+ /// When it is not, the tag is simply `openhuman@<ver>` with no `+`.
2112+ /// Either way the full tag must be non-empty.
2113+ #[ test]
2114+ fn sentry_release_tag_is_nonempty ( ) {
2115+ assert ! ( !build_sentry_release_tag( ) . is_empty( ) ) ;
2116+ }
2117+
2118+ // -------------------------------------------------------------------------
2119+ // resolve_sentry_environment
2120+ // -------------------------------------------------------------------------
2121+
2122+ #[ test]
2123+ fn sentry_environment_reads_openhuman_app_env ( ) {
2124+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
2125+ let key = "OPENHUMAN_APP_ENV" ;
2126+ let original = std:: env:: var ( key) . ok ( ) ;
2127+ std:: env:: set_var ( key, "staging" ) ;
2128+ let env = resolve_sentry_environment ( ) ;
2129+ match original {
2130+ Some ( v) => std:: env:: set_var ( key, v) ,
2131+ None => std:: env:: remove_var ( key) ,
2132+ }
2133+ assert_eq ! ( env, "staging" ) ;
2134+ }
2135+
2136+ #[ test]
2137+ fn sentry_environment_trims_whitespace_from_openhuman_app_env ( ) {
2138+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
2139+ let key = "OPENHUMAN_APP_ENV" ;
2140+ let original = std:: env:: var ( key) . ok ( ) ;
2141+ std:: env:: set_var ( key, " dev " ) ;
2142+ let env = resolve_sentry_environment ( ) ;
2143+ match original {
2144+ Some ( v) => std:: env:: set_var ( key, v) ,
2145+ None => std:: env:: remove_var ( key) ,
2146+ }
2147+ assert_eq ! ( env, "dev" ) ;
2148+ }
2149+
2150+ #[ test]
2151+ fn sentry_environment_skips_empty_openhuman_app_env ( ) {
2152+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
2153+ let key = "OPENHUMAN_APP_ENV" ;
2154+ let original = std:: env:: var ( key) . ok ( ) ;
2155+ std:: env:: set_var ( key, "" ) ;
2156+ let env = resolve_sentry_environment ( ) ;
2157+ match original {
2158+ Some ( v) => std:: env:: set_var ( key, v) ,
2159+ None => std:: env:: remove_var ( key) ,
2160+ }
2161+ // Falls through to VITE_ compile-time value or "production"; must be non-empty.
2162+ assert ! ( !env. is_empty( ) ) ;
2163+ }
2164+
2165+ #[ test]
2166+ fn sentry_environment_skips_whitespace_only_openhuman_app_env ( ) {
2167+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
2168+ let key = "OPENHUMAN_APP_ENV" ;
2169+ let original = std:: env:: var ( key) . ok ( ) ;
2170+ std:: env:: set_var ( key, " " ) ;
2171+ let env = resolve_sentry_environment ( ) ;
2172+ match original {
2173+ Some ( v) => std:: env:: set_var ( key, v) ,
2174+ None => std:: env:: remove_var ( key) ,
2175+ }
2176+ assert ! ( !env. is_empty( ) ) ;
2177+ }
2178+
2179+ /// When neither runtime env var nor compile-time VITE_ is set, the fallback
2180+ /// must be "production". Guard with a compile-time check so this test only
2181+ /// asserts the hard default when no compile-time override is present.
2182+ #[ test]
2183+ fn sentry_environment_defaults_to_production_when_unset ( ) {
2184+ let _g = ENV_LOCK . lock ( ) . unwrap ( ) ;
2185+ if option_env ! ( "VITE_OPENHUMAN_APP_ENV" ) . is_some ( ) {
2186+ // A compile-time override is baked in; skip — the fallback path is
2187+ // exercised by sentry_environment_skips_empty_openhuman_app_env.
2188+ return ;
2189+ }
2190+ let key = "OPENHUMAN_APP_ENV" ;
2191+ let original = std:: env:: var ( key) . ok ( ) ;
2192+ std:: env:: remove_var ( key) ;
2193+ let env = resolve_sentry_environment ( ) ;
2194+ match original {
2195+ Some ( v) => std:: env:: set_var ( key, v) ,
2196+ None => std:: env:: remove_var ( key) ,
2197+ }
2198+ assert_eq ! ( env, "production" ) ;
2199+ }
19862200}
0 commit comments