app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

runtime.rs (22132B)


      1 use std::{
      2     path::PathBuf,
      3     time::{SystemTime, UNIX_EPOCH},
      4 };
      5 
      6 use radroots_local_events::normalize_relay_url;
      7 use serde::Serialize;
      8 use thiserror::Error;
      9 
     10 use crate::{AppRuntimePathsError, AppRuntimeRoots};
     11 
     12 pub const APP_ID: &str = "org.radroots.app";
     13 pub const APP_NAME: &str = "Radroots";
     14 pub const APP_PLATFORM_RUNTIME: &str = "app-macos-native";
     15 pub const APP_PROJECTION_SOURCE: &str = "gpui-native";
     16 pub const APP_RUNTIME_ORIGIN: &str = "gpui://localhost";
     17 pub const APP_HOST_PLATFORM: &str = "desktop";
     18 pub const APP_RUNTIME_MODE_ENV: &str = "RADROOTS_APP_RUNTIME_MODE";
     19 pub const APP_NOSTR_RELAY_URLS_ENV: &str = "RADROOTS_APP_NOSTR_RELAY_URLS";
     20 pub const APP_LOCAL_LOG_ROOT_ENV: &str = "RADROOTS_APP_LOCAL_LOG_ROOT";
     21 
     22 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     23 pub enum AppRuntimeMode {
     24     LocalhostDev,
     25     Development,
     26     Production,
     27 }
     28 
     29 #[derive(Clone, Debug, Eq, PartialEq)]
     30 pub struct AppRuntimeConfig {
     31     pub runtime_mode: AppRuntimeMode,
     32     pub nostr_relay_urls: Vec<String>,
     33     pub local_log_root: PathBuf,
     34 }
     35 
     36 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
     37 pub struct AppBuildIdentity {
     38     pub package_name: String,
     39     pub package_version: String,
     40     pub build_profile: String,
     41     pub target_triple: String,
     42     pub projection_source: String,
     43     pub git_commit: Option<String>,
     44 }
     45 
     46 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
     47 pub struct AppCoreRuntimeMetadata {
     48     pub package_name: String,
     49     pub package_version: String,
     50     pub package_authors: String,
     51     pub rust_edition: String,
     52     pub rust_toolchain: String,
     53 }
     54 
     55 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
     56 pub struct AppHostRuntimeMetadata {
     57     pub app_identifier: String,
     58     pub app_name: String,
     59     pub app_version: String,
     60     pub app_build: String,
     61     pub platform_name: String,
     62     pub operating_system: String,
     63     pub host_locale: String,
     64     pub runtime_origin: String,
     65 }
     66 
     67 #[derive(Clone, Debug, Eq, PartialEq)]
     68 pub struct AppRuntimeCapture {
     69     pub host_locale: String,
     70     pub operating_system: String,
     71     pub run_id: String,
     72 }
     73 
     74 #[derive(Clone, Debug, Eq, PartialEq)]
     75 pub struct AppRuntimeSnapshot {
     76     pub title: String,
     77     pub runtime_mode: AppRuntimeMode,
     78     pub run_id: String,
     79     pub core: AppCoreRuntimeMetadata,
     80     pub build: AppBuildIdentity,
     81     pub host: AppHostRuntimeMetadata,
     82 }
     83 
     84 #[derive(Debug, Error)]
     85 pub enum AppRuntimeConfigError {
     86     #[error(transparent)]
     87     RuntimePaths(#[from] AppRuntimePathsError),
     88     #[error("missing required runtime env: {0}")]
     89     MissingEnv(&'static str),
     90     #[error("unsupported runtime mode: {0}")]
     91     UnsupportedRuntimeMode(String),
     92     #[error("missing required runtime config field: {0}")]
     93     MissingField(&'static str),
     94     #[error("invalid runtime relay url in {field}: {value}")]
     95     InvalidRelayUrl { field: &'static str, value: String },
     96 }
     97 
     98 impl AppRuntimeConfig {
     99     pub fn from_env() -> Result<Self, AppRuntimeConfigError> {
    100         Self::from_env_with(|name| std::env::var(name).ok(), None)
    101     }
    102 
    103     fn from_env_with<F>(
    104         mut read_env: F,
    105         default_log_root: Option<PathBuf>,
    106     ) -> Result<Self, AppRuntimeConfigError>
    107     where
    108         F: FnMut(&str) -> Option<String>,
    109     {
    110         let runtime_mode =
    111             parse_config_runtime_mode(&require_env_value(&mut read_env, APP_RUNTIME_MODE_ENV)?)?;
    112         let nostr_relay_urls = parse_relay_url_set(
    113             APP_NOSTR_RELAY_URLS_ENV,
    114             require_env_value(&mut read_env, APP_NOSTR_RELAY_URLS_ENV)?,
    115         )?;
    116         let local_log_root = read_env(APP_LOCAL_LOG_ROOT_ENV)
    117             .map(|value| require_path_value(APP_LOCAL_LOG_ROOT_ENV, value))
    118             .transpose()?;
    119         let local_log_root = match local_log_root {
    120             Some(local_log_root) => local_log_root,
    121             None => match default_log_root {
    122                 Some(default_log_root) => default_log_root,
    123                 None => AppRuntimeRoots::current_desktop()?.logs,
    124             },
    125         };
    126 
    127         Ok(Self {
    128             runtime_mode,
    129             nostr_relay_urls,
    130             local_log_root,
    131         })
    132     }
    133 }
    134 
    135 impl AppRuntimeCapture {
    136     pub fn current(mode: &AppRuntimeMode) -> Self {
    137         Self {
    138             host_locale: detect_host_locale(),
    139             operating_system: std::env::consts::OS.to_owned(),
    140             run_id: build_run_id(mode),
    141         }
    142     }
    143 }
    144 
    145 impl AppRuntimeSnapshot {
    146     pub fn capture(build: AppBuildIdentity) -> Self {
    147         let mode = parse_build_runtime_mode(&build.build_profile);
    148         Self::capture_for_mode(build, mode)
    149     }
    150 
    151     pub fn capture_for_mode(build: AppBuildIdentity, runtime_mode: AppRuntimeMode) -> Self {
    152         Self::from_capture(
    153             build,
    154             runtime_mode,
    155             AppRuntimeCapture::current(&runtime_mode),
    156         )
    157     }
    158 
    159     pub fn from_capture(
    160         build: AppBuildIdentity,
    161         runtime_mode: AppRuntimeMode,
    162         capture: AppRuntimeCapture,
    163     ) -> Self {
    164         let app_version = build.package_version.clone();
    165         let app_build = build
    166             .git_commit
    167             .clone()
    168             .unwrap_or_else(|| build.build_profile.clone());
    169 
    170         Self {
    171             title: APP_NAME.to_owned(),
    172             runtime_mode,
    173             run_id: capture.run_id,
    174             core: AppCoreRuntimeMetadata {
    175                 package_name: env!("CARGO_PKG_NAME").to_owned(),
    176                 package_version: env!("CARGO_PKG_VERSION").to_owned(),
    177                 package_authors: env!("CARGO_PKG_AUTHORS").to_owned(),
    178                 rust_edition: "2024".to_owned(),
    179                 rust_toolchain: env!("CARGO_PKG_RUST_VERSION").to_owned(),
    180             },
    181             build,
    182             host: AppHostRuntimeMetadata {
    183                 app_identifier: APP_ID.to_owned(),
    184                 app_name: APP_NAME.to_owned(),
    185                 app_version,
    186                 app_build,
    187                 platform_name: APP_HOST_PLATFORM.to_owned(),
    188                 operating_system: capture.operating_system,
    189                 host_locale: capture.host_locale,
    190                 runtime_origin: APP_RUNTIME_ORIGIN.to_owned(),
    191             },
    192         }
    193     }
    194 }
    195 
    196 pub fn runtime_mode_label(mode: &AppRuntimeMode) -> &'static str {
    197     match mode {
    198         AppRuntimeMode::LocalhostDev => "localhost-dev",
    199         AppRuntimeMode::Development => "development",
    200         AppRuntimeMode::Production => "production",
    201     }
    202 }
    203 
    204 fn parse_build_runtime_mode(build_profile: &str) -> AppRuntimeMode {
    205     match build_profile.trim() {
    206         "release" => AppRuntimeMode::Production,
    207         _ => AppRuntimeMode::Development,
    208     }
    209 }
    210 
    211 fn parse_config_runtime_mode(value: &str) -> Result<AppRuntimeMode, AppRuntimeConfigError> {
    212     match value.trim() {
    213         "localhost-dev" => Ok(AppRuntimeMode::LocalhostDev),
    214         "development" => Ok(AppRuntimeMode::Development),
    215         "production" => Ok(AppRuntimeMode::Production),
    216         other => Err(AppRuntimeConfigError::UnsupportedRuntimeMode(
    217             other.to_owned(),
    218         )),
    219     }
    220 }
    221 
    222 fn parse_relay_url_set(
    223     field: &'static str,
    224     value: String,
    225 ) -> Result<Vec<String>, AppRuntimeConfigError> {
    226     let mut relays = Vec::new();
    227     for relay in value.split(',') {
    228         let relay = relay.trim();
    229         if relay.is_empty() {
    230             return Err(AppRuntimeConfigError::InvalidRelayUrl {
    231                 field,
    232                 value: relay.to_owned(),
    233             });
    234         }
    235         let normalized = normalize_app_relay_url(field, relay).map_err(|_| {
    236             AppRuntimeConfigError::InvalidRelayUrl {
    237                 field,
    238                 value: relay.to_owned(),
    239             }
    240         })?;
    241         if !relays.iter().any(|existing| existing == &normalized) {
    242             relays.push(normalized);
    243         }
    244     }
    245 
    246     if relays.is_empty() {
    247         return Err(AppRuntimeConfigError::MissingField(field));
    248     }
    249 
    250     Ok(relays)
    251 }
    252 
    253 fn normalize_app_relay_url(
    254     field: &'static str,
    255     relay: &str,
    256 ) -> Result<String, AppRuntimeConfigError> {
    257     normalize_relay_url(relay).map_err(|_| AppRuntimeConfigError::InvalidRelayUrl {
    258         field,
    259         value: relay.to_owned(),
    260     })
    261 }
    262 
    263 fn require_path_value(
    264     field: &'static str,
    265     value: String,
    266 ) -> Result<PathBuf, AppRuntimeConfigError> {
    267     let trimmed = value.trim();
    268     if trimmed.is_empty() {
    269         return Err(AppRuntimeConfigError::MissingField(field));
    270     }
    271 
    272     Ok(PathBuf::from(trimmed))
    273 }
    274 
    275 fn require_value(field: &'static str, value: String) -> Result<String, AppRuntimeConfigError> {
    276     let trimmed = value.trim();
    277     if trimmed.is_empty() {
    278         return Err(AppRuntimeConfigError::MissingField(field));
    279     }
    280 
    281     Ok(trimmed.to_owned())
    282 }
    283 
    284 fn require_env_value<F>(
    285     read_env: &mut F,
    286     field: &'static str,
    287 ) -> Result<String, AppRuntimeConfigError>
    288 where
    289     F: FnMut(&str) -> Option<String>,
    290 {
    291     let value = read_env(field).ok_or(AppRuntimeConfigError::MissingEnv(field))?;
    292     require_value(field, value)
    293 }
    294 
    295 fn detect_host_locale() -> String {
    296     [
    297         std::env::var("LC_ALL").ok(),
    298         std::env::var("LC_MESSAGES").ok(),
    299         std::env::var("LANGUAGE").ok(),
    300         std::env::var("LANG").ok(),
    301     ]
    302     .into_iter()
    303     .flatten()
    304     .find_map(|value| {
    305         let trimmed = value.trim();
    306         if trimmed.is_empty() {
    307             None
    308         } else {
    309             Some(trimmed.to_owned())
    310         }
    311     })
    312     .unwrap_or_else(|| "en".to_owned())
    313 }
    314 
    315 fn build_run_id(mode: &AppRuntimeMode) -> String {
    316     let started_at_ms = SystemTime::now()
    317         .duration_since(UNIX_EPOCH)
    318         .unwrap_or_default()
    319         .as_millis();
    320     format!(
    321         "run-{}-{started_at_ms}-pid{}",
    322         runtime_mode_label(mode),
    323         std::process::id()
    324     )
    325 }
    326 
    327 #[cfg(test)]
    328 mod tests {
    329     use std::{collections::BTreeMap, path::PathBuf};
    330 
    331     use super::{
    332         APP_HOST_PLATFORM, APP_ID, APP_LOCAL_LOG_ROOT_ENV, APP_NAME, APP_NOSTR_RELAY_URLS_ENV,
    333         APP_PROJECTION_SOURCE, APP_RUNTIME_MODE_ENV, APP_RUNTIME_ORIGIN, AppBuildIdentity,
    334         AppRuntimeCapture, AppRuntimeConfig, AppRuntimeConfigError, AppRuntimeMode,
    335         AppRuntimeSnapshot, runtime_mode_label,
    336     };
    337 
    338     fn test_build_identity() -> AppBuildIdentity {
    339         AppBuildIdentity {
    340             package_name: "radroots_app".to_owned(),
    341             package_version: "0.1.0".to_owned(),
    342             build_profile: "debug".to_owned(),
    343             target_triple: "aarch64-apple-darwin".to_owned(),
    344             projection_source: APP_PROJECTION_SOURCE.to_owned(),
    345             git_commit: Some("deadbeefcafefeed".to_owned()),
    346         }
    347     }
    348 
    349     fn test_runtime_env() -> BTreeMap<&'static str, String> {
    350         BTreeMap::from([
    351             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    352             (
    353                 APP_NOSTR_RELAY_URLS_ENV,
    354                 " ws://127.0.0.1:8080 , ws://127.0.0.1:8081 , ws://127.0.0.1:8080 ".to_owned(),
    355             ),
    356         ])
    357     }
    358 
    359     #[test]
    360     fn runtime_snapshot_surfaces_core_and_host_metadata() {
    361         let snapshot = AppRuntimeSnapshot::from_capture(
    362             test_build_identity(),
    363             AppRuntimeMode::Development,
    364             AppRuntimeCapture {
    365                 host_locale: "en_US.UTF-8".to_owned(),
    366                 operating_system: "macos".to_owned(),
    367                 run_id: "run-development-123-pid456".to_owned(),
    368             },
    369         );
    370 
    371         assert_eq!(snapshot.title, APP_NAME);
    372         assert_eq!(snapshot.run_id, "run-development-123-pid456");
    373         assert_eq!(snapshot.core.package_name, "radroots_app_core");
    374         assert_eq!(snapshot.core.package_version, env!("CARGO_PKG_VERSION"));
    375         assert_eq!(snapshot.core.package_authors, env!("CARGO_PKG_AUTHORS"));
    376         assert_eq!(snapshot.core.rust_edition, "2024");
    377         assert_eq!(snapshot.core.rust_toolchain, env!("CARGO_PKG_RUST_VERSION"));
    378         assert_eq!(snapshot.build.package_name, "radroots_app");
    379         assert_eq!(snapshot.build.target_triple, "aarch64-apple-darwin");
    380         assert_eq!(snapshot.host.app_identifier, APP_ID);
    381         assert_eq!(snapshot.host.app_name, APP_NAME);
    382         assert_eq!(snapshot.host.app_version, env!("CARGO_PKG_VERSION"));
    383         assert_eq!(snapshot.host.app_build, "deadbeefcafefeed");
    384         assert_eq!(snapshot.host.platform_name, APP_HOST_PLATFORM);
    385         assert_eq!(snapshot.host.operating_system, "macos");
    386         assert_eq!(snapshot.host.host_locale, "en_US.UTF-8");
    387         assert_eq!(snapshot.host.runtime_origin, APP_RUNTIME_ORIGIN);
    388     }
    389 
    390     #[test]
    391     fn runtime_config_requires_explicit_runtime_mode_env() {
    392         let env = BTreeMap::from([(APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned())]);
    393         let error = AppRuntimeConfig::from_env_with(
    394             |name| env.get(name).cloned(),
    395             Some(PathBuf::from("/tmp/default-logs")),
    396         )
    397         .expect_err("missing runtime mode env should fail");
    398 
    399         assert!(matches!(
    400             error,
    401             AppRuntimeConfigError::MissingEnv(APP_RUNTIME_MODE_ENV)
    402         ));
    403     }
    404 
    405     #[test]
    406     fn runtime_config_surfaces_explicit_local_log_root() {
    407         let env = BTreeMap::from([
    408             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    409             (APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned()),
    410             (APP_LOCAL_LOG_ROOT_ENV, "/tmp/radroots/logs".to_owned()),
    411         ]);
    412         let config = AppRuntimeConfig::from_env_with(
    413             |name| env.get(name).cloned(),
    414             Some(PathBuf::from("/tmp/default-logs")),
    415         )
    416         .expect("valid env config");
    417 
    418         assert_eq!(config.runtime_mode, AppRuntimeMode::LocalhostDev);
    419         assert_eq!(config.nostr_relay_urls, vec!["ws://127.0.0.1:8080"]);
    420         assert_eq!(config.local_log_root, PathBuf::from("/tmp/radroots/logs"));
    421     }
    422 
    423     #[test]
    424     fn runtime_config_normalizes_configured_nostr_relay_urls() {
    425         let env = test_runtime_env();
    426         let config = AppRuntimeConfig::from_env_with(
    427             |name| env.get(name).cloned(),
    428             Some(PathBuf::from("/tmp/default-logs")),
    429         )
    430         .expect("valid env config");
    431 
    432         assert_eq!(
    433             config.nostr_relay_urls,
    434             vec!["ws://127.0.0.1:8080", "ws://127.0.0.1:8081"]
    435         );
    436     }
    437 
    438     #[test]
    439     fn runtime_config_rejects_malformed_nostr_relay_urls() {
    440         let env = BTreeMap::from([
    441             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    442             (APP_NOSTR_RELAY_URLS_ENV, "not-a-url".to_owned()),
    443         ]);
    444         let error = AppRuntimeConfig::from_env_with(
    445             |name| env.get(name).cloned(),
    446             Some(PathBuf::from("/tmp/default-logs")),
    447         )
    448         .expect_err("malformed relay url should fail");
    449 
    450         assert!(
    451             matches!(
    452                 error,
    453                 AppRuntimeConfigError::InvalidRelayUrl {
    454                     field: APP_NOSTR_RELAY_URLS_ENV,
    455                     ref value
    456                 } if value == "not-a-url"
    457             ),
    458             "unexpected error: {error}"
    459         );
    460     }
    461 
    462     #[test]
    463     fn runtime_config_rejects_non_websocket_nostr_relay_urls() {
    464         let env = BTreeMap::from([
    465             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    466             (APP_NOSTR_RELAY_URLS_ENV, "https://relay.example".to_owned()),
    467         ]);
    468         let error = AppRuntimeConfig::from_env_with(
    469             |name| env.get(name).cloned(),
    470             Some(PathBuf::from("/tmp/default-logs")),
    471         )
    472         .expect_err("non-websocket relay url should fail");
    473 
    474         assert!(
    475             matches!(
    476                 error,
    477                 AppRuntimeConfigError::InvalidRelayUrl {
    478                     field: APP_NOSTR_RELAY_URLS_ENV,
    479                     ref value
    480                 } if value == "https://relay.example"
    481             ),
    482             "unexpected error: {error}"
    483         );
    484     }
    485 
    486     #[test]
    487     fn runtime_config_rejects_hostless_nostr_relay_urls() {
    488         let env = BTreeMap::from([
    489             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    490             (APP_NOSTR_RELAY_URLS_ENV, "wss://".to_owned()),
    491         ]);
    492         let error = AppRuntimeConfig::from_env_with(
    493             |name| env.get(name).cloned(),
    494             Some(PathBuf::from("/tmp/default-logs")),
    495         )
    496         .expect_err("hostless relay url should fail");
    497 
    498         assert!(
    499             matches!(
    500                 error,
    501                 AppRuntimeConfigError::InvalidRelayUrl {
    502                     field: APP_NOSTR_RELAY_URLS_ENV,
    503                     ref value
    504                 } if value == "wss://"
    505             ),
    506             "unexpected error: {error}"
    507         );
    508     }
    509 
    510     #[test]
    511     fn runtime_config_rejects_malformed_nostr_relay_authority() {
    512         for relay_url in [
    513             "wss://user@relay.example",
    514             "wss://relay.example:abc",
    515             "wss://2001:db8::1",
    516             "wss://relay.example,,wss://relay-two.example",
    517         ] {
    518             let env = BTreeMap::from([
    519                 (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    520                 (APP_NOSTR_RELAY_URLS_ENV, relay_url.to_owned()),
    521             ]);
    522             let error = AppRuntimeConfig::from_env_with(
    523                 |name| env.get(name).cloned(),
    524                 Some(PathBuf::from("/tmp/default-logs")),
    525             )
    526             .expect_err("malformed relay authority should fail");
    527 
    528             assert!(
    529                 matches!(
    530                     error,
    531                     AppRuntimeConfigError::InvalidRelayUrl {
    532                         field: APP_NOSTR_RELAY_URLS_ENV,
    533                         ..
    534                     }
    535                 ),
    536                 "unexpected error for {relay_url}: {error}"
    537             );
    538         }
    539     }
    540 
    541     #[test]
    542     fn runtime_config_accepts_bracketed_ipv6_nostr_relay_urls() {
    543         let env = BTreeMap::from([
    544             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    545             (
    546                 APP_NOSTR_RELAY_URLS_ENV,
    547                 " wss://[2001:db8::1]:443/relay ".to_owned(),
    548             ),
    549         ]);
    550         let config = AppRuntimeConfig::from_env_with(
    551             |name| env.get(name).cloned(),
    552             Some(PathBuf::from("/tmp/default-logs")),
    553         )
    554         .expect("ipv6 relay url should resolve");
    555 
    556         assert_eq!(
    557             config.nostr_relay_urls,
    558             vec!["wss://[2001:db8::1]:443/relay"]
    559         );
    560     }
    561 
    562     #[test]
    563     fn runtime_config_defaults_local_log_root_from_runtime_paths() {
    564         let env = test_runtime_env();
    565         let config = AppRuntimeConfig::from_env_with(
    566             |name| env.get(name).cloned(),
    567             Some(PathBuf::from("/tmp/default-logs")),
    568         )
    569         .expect("default log root should apply");
    570 
    571         assert_eq!(config.local_log_root, PathBuf::from("/tmp/default-logs"));
    572     }
    573 
    574     #[test]
    575     fn runtime_config_accepts_explicit_log_root_without_default_runtime_paths() {
    576         let env = BTreeMap::from([
    577             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    578             (APP_NOSTR_RELAY_URLS_ENV, "ws://127.0.0.1:8080".to_owned()),
    579             (APP_LOCAL_LOG_ROOT_ENV, "/tmp/explicit-logs".to_owned()),
    580         ]);
    581         let config = AppRuntimeConfig::from_env_with(|name| env.get(name).cloned(), None)
    582             .expect("explicit local log root should bypass runtime root discovery");
    583 
    584         assert_eq!(config.local_log_root, PathBuf::from("/tmp/explicit-logs"));
    585     }
    586 
    587     #[test]
    588     fn runtime_snapshot_falls_back_to_build_profile_when_git_commit_is_missing() {
    589         let mut build = test_build_identity();
    590         build.git_commit = None;
    591         build.build_profile = "release".to_owned();
    592 
    593         let snapshot = AppRuntimeSnapshot::from_capture(
    594             build,
    595             AppRuntimeMode::Production,
    596             AppRuntimeCapture {
    597                 host_locale: "en".to_owned(),
    598                 operating_system: "linux".to_owned(),
    599                 run_id: "run-production-123-pid456".to_owned(),
    600             },
    601         );
    602 
    603         assert_eq!(snapshot.host.app_build, "release");
    604         assert_eq!(runtime_mode_label(&snapshot.runtime_mode), "production");
    605     }
    606 
    607     #[test]
    608     fn runtime_snapshot_capture_for_mode_uses_rust_owned_host_identity() {
    609         let snapshot = AppRuntimeSnapshot::capture_for_mode(
    610             test_build_identity(),
    611             AppRuntimeMode::LocalhostDev,
    612         );
    613 
    614         assert_eq!(snapshot.title, APP_NAME);
    615         assert!(snapshot.run_id.starts_with("run-localhost-dev-"));
    616         assert!(snapshot.run_id.contains("-pid"));
    617         assert_eq!(snapshot.host.app_identifier, APP_ID);
    618         assert_eq!(snapshot.host.app_name, APP_NAME);
    619         assert_eq!(snapshot.host.app_version, env!("CARGO_PKG_VERSION"));
    620         assert_eq!(snapshot.host.platform_name, APP_HOST_PLATFORM);
    621         assert_eq!(snapshot.host.operating_system, std::env::consts::OS);
    622         assert_eq!(snapshot.host.runtime_origin, APP_RUNTIME_ORIGIN);
    623         assert!(!snapshot.host.host_locale.trim().is_empty());
    624     }
    625 
    626     #[test]
    627     fn runtime_config_rejects_empty_required_fields() {
    628         let env = BTreeMap::from([
    629             (APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned()),
    630             (APP_NOSTR_RELAY_URLS_ENV, "".to_owned()),
    631         ]);
    632         let error = AppRuntimeConfig::from_env_with(
    633             |name| env.get(name).cloned(),
    634             Some(PathBuf::from("/tmp/default-logs")),
    635         )
    636         .expect_err("missing relay env should fail");
    637 
    638         assert!(
    639             matches!(
    640                 error,
    641                 AppRuntimeConfigError::MissingField(APP_NOSTR_RELAY_URLS_ENV)
    642             ),
    643             "unexpected error: {error}"
    644         );
    645     }
    646 
    647     #[test]
    648     fn runtime_config_rejects_missing_nostr_relay_urls() {
    649         let env = BTreeMap::from([(APP_RUNTIME_MODE_ENV, "localhost-dev".to_owned())]);
    650         let error = AppRuntimeConfig::from_env_with(
    651             |name| env.get(name).cloned(),
    652             Some(PathBuf::from("/tmp/default-logs")),
    653         )
    654         .expect_err("missing relay urls should fail");
    655 
    656         assert!(
    657             matches!(
    658                 error,
    659                 AppRuntimeConfigError::MissingEnv(APP_NOSTR_RELAY_URLS_ENV)
    660             ),
    661             "unexpected error: {error}"
    662         );
    663     }
    664 }