use codex_utils_absolute_path::AbsolutePathBuf;use schemars::JsonSchema;use schemars::r#gen::SchemaGenerator;use schemars::schema::InstanceType;use schemars::schema::Metadata;use schemars::schema::Schema;use schemars::schema::SchemaObject;use serde::Deserialize;use serde::Serialize;use serde_json::Value;use std::collections::HashMap;use std::fmt;use std::num::NonZeroU64;use std::ops::Deref;use std::str::FromStr;use std::time::Duration;use strum_macros::Display;use strum_macros::EnumIter;use ts_rs::TS;use wildmatch::WildMatchPattern;use crate::openai_models::ReasoningEffort;/// Selects which part of the active context is charged against/// `model_auto_compact_token_limit`.#[derive( Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,)]#[serde(rename_all = "snake_case")]#[strum(serialize_all = "snake_case")]pub enum AutoCompactTokenLimitScope { /// Count the full active context against the limit. #[default] Total, /// Count sampled output and later growth after the carried window prefix. BodyAfterPrefix,}/// A summary of the reasoning performed by the model. This can be useful for/// debugging and understanding the model's reasoning process./// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries#[derive( Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum ReasoningSummary { #[default] Auto, Concise, Detailed, /// Option to disable reasoning summaries. None,}/// Controls output length/detail on GPT-5 models via the Responses API./// Serialized with lowercase values to match the OpenAI API.#[derive( Hash, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum Verbosity { Low, #[default] Medium, High,}#[derive( Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display, JsonSchema, TS,)]#[serde(rename_all = "kebab-case")]#[strum(serialize_all = "kebab-case")]pub enum SandboxMode { #[serde(rename = "read-only")] #[default] ReadOnly, #[serde(rename = "workspace-write")] WorkspaceWrite, #[serde(rename = "danger-full-access")] DangerFullAccess,}/// Validated plain profile-v2 name used to select `$CODEX_HOME/<name>.config.toml`.#[derive(Clone, Debug, PartialEq, Eq)]pub struct ProfileV2Name(String);impl ProfileV2Name { pub fn as_str(&self) -> &str { &self.0 }}#[derive(Debug, PartialEq, Eq)]pub struct ProfileV2NameParseError { value: String,}impl fmt::Display for ProfileV2NameParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "invalid --profile value `{}`; pass a plain name such as `work`", self.value ) }}impl std::error::Error for ProfileV2NameParseError {}impl FromStr for ProfileV2Name { type Err = ProfileV2NameParseError; fn from_str(value: &str) -> Result<Self, Self::Err> { if value.is_empty() || !value .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')) { return Err(ProfileV2NameParseError { value: value.to_string(), }); } Ok(Self(value.to_string())) }}impl Deref for ProfileV2Name { type Target = str; fn deref(&self) -> &Self::Target { self.as_str() }}impl fmt::Display for ProfileV2Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) }}#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, TS)]#[strum(serialize_all = "snake_case")]#[ts(type = r#""user" | "auto_review" | "guardian_subagent""#)]/// Configures who approval requests are routed to for review. Examples/// include sandbox escapes, blocked network access, MCP approval prompts, and/// ARC escalations. Defaults to `user`. `auto_review` uses a carefully/// prompted subagent to gather relevant context and apply a risk-based/// decision framework before approving or denying the request.pub enum ApprovalsReviewer { #[default] #[serde(rename = "user")] User, #[serde(rename = "auto_review", alias = "guardian_subagent")] #[strum(serialize = "auto_review")] AutoReview,}impl JsonSchema for ApprovalsReviewer { fn schema_name() -> String { "ApprovalsReviewer".to_string() } fn json_schema(_generator: &mut SchemaGenerator) -> Schema { string_enum_schema_with_description( &["user", "auto_review", "guardian_subagent"], "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.", ) }}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]#[serde(rename_all = "kebab-case")]pub enum ShellEnvironmentPolicyInherit { /// "Core" environment variables for the platform. On UNIX, this would /// include HOME, LOGNAME, PATH, SHELL, and USER, among others. Core, /// Inherits the full environment from the parent process. #[default] All, /// Do not inherit any environment variables from the parent process. None,}pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;/// Deriving the `env` based on this policy works as follows:/// 1. Create an initial map based on the `inherit` policy./// 2. If `ignore_default_excludes` is false, filter the map using the default/// exclude pattern(s), which are: `"*KEY*"`, `"*SECRET*"`, and `"*TOKEN*"`./// 3. If `exclude` is not empty, filter the map using the provided patterns./// 4. Insert any entries from `r#set` into the map./// 5. If non-empty, filter the map using the `include_only` patterns.#[derive(Debug, Clone, PartialEq)]pub struct ShellEnvironmentPolicy { /// Starting point when building the environment. pub inherit: ShellEnvironmentPolicyInherit, /// True to skip the check to exclude default environment variables that /// contain "KEY", "SECRET", or "TOKEN" in their name. Defaults to true. pub ignore_default_excludes: bool, /// Environment variable names to exclude from the environment. pub exclude: Vec<EnvironmentVariablePattern>, /// (key, value) pairs to insert in the environment. pub r#set: HashMap<String, String>, /// Environment variable names to retain in the environment. pub include_only: Vec<EnvironmentVariablePattern>, /// If true, the shell profile will be used to run the command. pub use_profile: bool,}impl Default for ShellEnvironmentPolicy { fn default() -> Self { Self { inherit: ShellEnvironmentPolicyInherit::All, ignore_default_excludes: true, exclude: Vec::new(), r#set: HashMap::new(), include_only: Vec::new(), use_profile: false, } }}fn string_enum_schema_with_description(values: &[&str], description: &str) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::String.into()), metadata: Some(Box::new(Metadata { description: Some(description.to_string()), ..Default::default() })), ..Default::default() }; schema.enum_values = Some( values .iter() .map(|value| Value::String((*value).to_string())) .collect(), ); Schema::Object(schema)}#[derive( Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS,)]#[serde(rename_all = "kebab-case")]#[strum(serialize_all = "kebab-case")]pub enum WindowsSandboxLevel { #[default] Disabled, RestrictedToken, Elevated,}#[derive( Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, PartialOrd, Ord, EnumIter,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum Personality { None, Friendly, Pragmatic,}#[derive( Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum WebSearchMode { Disabled, #[default] Cached, Live,}#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum WebSearchContextSize { Low, Medium, High,}#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[schemars(deny_unknown_fields)]pub struct WebSearchLocation { pub country: Option<String>, pub region: Option<String>, pub city: Option<String>, pub timezone: Option<String>,}impl WebSearchLocation { pub fn merge(&self, other: &Self) -> Self { Self { country: other.country.clone().or_else(|| self.country.clone()), region: other.region.clone().or_else(|| self.region.clone()), city: other.city.clone().or_else(|| self.city.clone()), timezone: other.timezone.clone().or_else(|| self.timezone.clone()), } }}#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[schemars(deny_unknown_fields)]pub struct WebSearchToolConfig { pub context_size: Option<WebSearchContextSize>, pub allowed_domains: Option<Vec<String>>, pub location: Option<WebSearchLocation>,}impl WebSearchToolConfig { pub fn merge(&self, other: &Self) -> Self { Self { context_size: other.context_size.or(self.context_size), allowed_domains: other .allowed_domains .clone() .or_else(|| self.allowed_domains.clone()), location: match (&self.location, &other.location) { (Some(location), Some(other_location)) => Some(location.merge(other_location)), (Some(location), None) => Some(location.clone()), (None, Some(other_location)) => Some(other_location.clone()), (None, None) => None, }, } }}#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[schemars(deny_unknown_fields)]pub struct WebSearchFilters { pub allowed_domains: Option<Vec<String>>,}#[derive( Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Display, JsonSchema, TS,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum WebSearchUserLocationType { #[default] Approximate,}#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[schemars(deny_unknown_fields)]pub struct WebSearchUserLocation { #[serde(default)] pub r#type: WebSearchUserLocationType, pub country: Option<String>, pub region: Option<String>, pub city: Option<String>, pub timezone: Option<String>,}#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]#[schemars(deny_unknown_fields)]pub struct WebSearchConfig { pub filters: Option<WebSearchFilters>, pub user_location: Option<WebSearchUserLocation>, pub search_context_size: Option<WebSearchContextSize>,}impl From<WebSearchLocation> for WebSearchUserLocation { fn from(location: WebSearchLocation) -> Self { Self { r#type: WebSearchUserLocationType::Approximate, country: location.country, region: location.region, city: location.city, timezone: location.timezone, } }}impl From<WebSearchToolConfig> for WebSearchConfig { fn from(config: WebSearchToolConfig) -> Self { Self { filters: config .allowed_domains .map(|allowed_domains| WebSearchFilters { allowed_domains: Some(allowed_domains), }), user_location: config.location.map(Into::into), search_context_size: config.context_size, } }}#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum ServiceTier { Fast, Flex,}/// Request/config sentinel for explicit standard routing.////// This is not a catalog service tier id. It means the user intentionally/// selected no service tier, so model catalog defaults should not apply.pub const SERVICE_TIER_DEFAULT_REQUEST_VALUE: &str = "default";impl ServiceTier { pub const fn request_value(self) -> &'static str { match self { Self::Fast => "priority", Self::Flex => "flex", } } pub fn from_request_value(value: &str) -> Option<Self> { match value { "fast" | "priority" => Some(Self::Fast), "flex" => Some(Self::Flex), _ => None, } }}#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum ForcedLoginMethod { Chatgpt, Api,}const DEFAULT_PROVIDER_AUTH_TIMEOUT_MS: u64 = 5_000;const DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS: u64 = 300_000;/// Configuration for obtaining a provider bearer token from a command.#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]#[schemars(deny_unknown_fields)]pub struct ModelProviderAuthInfo { /// Command to execute. Bare names are resolved via `PATH`; paths are resolved against `cwd`. pub command: String, /// Command arguments. #[serde(default)] pub args: Vec<String>, /// Maximum time to wait for the token command to exit successfully. #[serde(default = "default_provider_auth_timeout_ms")] pub timeout_ms: NonZeroU64, /// Maximum age for the cached token before rerunning the command. /// Set to `0` to disable proactive refresh and only rerun after a 401 retry path. #[serde(default = "default_provider_auth_refresh_interval_ms")] pub refresh_interval_ms: u64, /// Working directory used when running the token command. #[serde(default = "default_provider_auth_cwd")] #[schemars(skip_serializing_if = "is_default_provider_auth_cwd")] pub cwd: AbsolutePathBuf,}impl ModelProviderAuthInfo { pub fn timeout(&self) -> Duration { Duration::from_millis(self.timeout_ms.get()) } pub fn refresh_interval(&self) -> Option<Duration> { NonZeroU64::new(self.refresh_interval_ms).map(|value| Duration::from_millis(value.get())) }}fn default_provider_auth_timeout_ms() -> NonZeroU64 { non_zero_u64( DEFAULT_PROVIDER_AUTH_TIMEOUT_MS, "model_providers.<id>.auth.timeout_ms", )}fn default_provider_auth_refresh_interval_ms() -> u64 { DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS}fn non_zero_u64(value: u64, field_name: &str) -> NonZeroU64 { match NonZeroU64::new(value) { Some(value) => value, None => panic!("{field_name} must be non-zero"), }}fn default_provider_auth_cwd() -> AbsolutePathBuf { let deserializer = serde::de::value::StrDeserializer::<serde::de::value::Error>::new("."); if let Ok(cwd) = AbsolutePathBuf::deserialize(deserializer) { return cwd; } match AbsolutePathBuf::current_dir() { Ok(cwd) => cwd, Err(err) => panic!("provider auth cwd must resolve: {err}"), }}fn is_default_provider_auth_cwd(path: &AbsolutePathBuf) -> bool { path == &default_provider_auth_cwd()}/// Represents the trust level for a project directory./// This determines the approval policy and sandbox mode applied.#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum TrustLevel { Trusted, Untrusted,}/// Controls whether the TUI uses the terminal's alternate screen buffer.////// - `auto` (default): Use alternate screen mode./// - `always`: Always use alternate screen mode./// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback.////// The CLI flag `--no-alt-screen` can override this setting at runtime.#[derive( Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum AltScreenMode { /// Use alternate screen mode. #[default] Auto, /// Always use alternate screen mode. Always, /// Never use alternate screen (inline mode only). Never,}/// Initial collaboration mode to use when the TUI starts.#[derive( Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, TS, Default,)]#[serde(rename_all = "snake_case")]pub enum ModeKind { Plan, #[default] #[serde( alias = "code", alias = "pair_programming", alias = "execute", alias = "custom" )] Default, #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)] #[schemars(skip)] #[ts(skip)] PairProgramming, #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)] #[schemars(skip)] #[ts(skip)] Execute,}pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];impl ModeKind { pub const fn display_name(self) -> &'static str { match self { Self::Plan => "Plan", Self::Default => "Default", Self::PairProgramming => "Pair Programming", Self::Execute => "Execute", } } pub const fn is_tui_visible(self) -> bool { matches!(self, Self::Plan | Self::Default) } pub const fn allows_request_user_input(self) -> bool { matches!(self, Self::Plan) }}/// Collaboration mode for a Codex session.#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]#[serde(rename_all = "lowercase")]pub struct CollaborationMode { pub mode: ModeKind, pub settings: Settings,}impl CollaborationMode { /// Returns a reference to the settings. fn settings_ref(&self) -> &Settings { &self.settings } pub fn model(&self) -> &str { self.settings_ref().model.as_str() } pub fn reasoning_effort(&self) -> Option<ReasoningEffort> { self.settings_ref().reasoning_effort.clone() } /// Updates the collaboration mode with new model and/or effort values. /// /// - `model`: `Some(s)` to update the model, `None` to keep the current model /// - `effort`: `Some(Some(e))` to set effort to `e`, `Some(None)` to clear effort, `None` to keep current effort /// - `developer_instructions`: `Some(Some(s))` to set instructions, `Some(None)` to clear them, `None` to keep current /// /// Returns a new `CollaborationMode` with updated values, preserving the mode. pub fn with_updates( &self, model: Option<String>, effort: Option<Option<ReasoningEffort>>, developer_instructions: Option<Option<String>>, ) -> Self { let settings = self.settings_ref(); let updated_settings = Settings { model: model.unwrap_or_else(|| settings.model.clone()), reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()), developer_instructions: developer_instructions .unwrap_or_else(|| settings.developer_instructions.clone()), }; CollaborationMode { mode: self.mode, settings: updated_settings, } } /// Applies a mask to this collaboration mode, returning a new collaboration mode /// with the mask values applied. Fields in the mask that are `Some` will override /// the corresponding fields, while `None` values will preserve the original values. /// /// The `name` field in the mask is ignored as it's metadata for the mask itself. pub fn apply_mask(&self, mask: &CollaborationModeMask) -> Self { let settings = self.settings_ref(); CollaborationMode { mode: mask.mode.unwrap_or(self.mode), settings: Settings { model: mask.model.clone().unwrap_or_else(|| settings.model.clone()), reasoning_effort: mask .reasoning_effort .clone() .unwrap_or_else(|| settings.reasoning_effort.clone()), developer_instructions: mask .developer_instructions .clone() .unwrap_or_else(|| settings.developer_instructions.clone()), }, } }}/// Settings for a collaboration mode.#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]pub struct Settings { pub model: String, pub reasoning_effort: Option<ReasoningEffort>, pub developer_instructions: Option<String>,}/// A mask for collaboration mode settings, allowing partial updates./// All fields except `name` are optional, enabling selective updates.#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]pub struct CollaborationModeMask { pub name: String, pub mode: Option<ModeKind>, pub model: Option<String>, pub reasoning_effort: Option<Option<ReasoningEffort>>, pub developer_instructions: Option<Option<String>>,}#[cfg(test)]mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn apply_mask_can_clear_optional_fields() { let mode = CollaborationMode { mode: ModeKind::Default, settings: Settings { model: "gpt-5.2-codex".to_string(), reasoning_effort: Some(ReasoningEffort::High), developer_instructions: Some("stay focused".to_string()), }, }; let mask = CollaborationModeMask { name: "Clear".to_string(), mode: None, model: None, reasoning_effort: Some(None), developer_instructions: Some(None), }; let expected = CollaborationMode { mode: ModeKind::Default, settings: Settings { model: "gpt-5.2-codex".to_string(), reasoning_effort: None, developer_instructions: None, }, }; assert_eq!(expected, mode.apply_mask(&mask)); } #[test] fn mode_kind_deserializes_alias_values_to_default() { for alias in ["code", "pair_programming", "execute", "custom"] { let json = format!("\"{alias}\""); let mode: ModeKind = serde_json::from_str(&json).expect("deserialize mode"); assert_eq!(ModeKind::Default, mode); } } #[test] fn approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent() { assert_eq!(ApprovalsReviewer::User.to_string(), "user"); assert_eq!( serde_json::to_string(&ApprovalsReviewer::User).expect("serialize reviewer"), "\"user\"" ); assert_eq!( serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"), "\"auto_review\"" ); for value in ["user", "auto_review", "guardian_subagent"] { let json = format!("\"{value}\""); let reviewer: ApprovalsReviewer = serde_json::from_str(&json).expect("deserialize reviewer"); let expected = if value == "user" { ApprovalsReviewer::User } else { ApprovalsReviewer::AutoReview }; assert_eq!(expected, reviewer); } } #[test] fn profile_v2_name_rejects_paths_and_empty_names() { assert_eq!( ProfileV2Name::from_str("../foo"), Err(ProfileV2NameParseError { value: "../foo".to_string(), }), "dots and slashes are disallowed to prevent reading arbitrary files" ); assert_eq!( ProfileV2Name::from_str(""), Err(ProfileV2NameParseError { value: String::new(), }), "profile name cannot be empty" ); } #[test] fn tui_visible_collaboration_modes_match_mode_kind_visibility() { let expected = [ModeKind::Default, ModeKind::Plan]; assert_eq!(expected, TUI_VISIBLE_COLLABORATION_MODES); for mode in TUI_VISIBLE_COLLABORATION_MODES { assert!(mode.is_tui_visible()); } assert!(!ModeKind::PairProgramming.is_tui_visible()); assert!(!ModeKind::Execute.is_tui_visible()); } #[test] fn web_search_location_merge_prefers_overlay_values() { let base = WebSearchLocation { country: Some("US".to_string()), region: Some("CA".to_string()), city: None, timezone: Some("America/Los_Angeles".to_string()), }; let overlay = WebSearchLocation { country: None, region: Some("WA".to_string()), city: Some("Seattle".to_string()), timezone: None, }; let expected = WebSearchLocation { country: Some("US".to_string()), region: Some("WA".to_string()), city: Some("Seattle".to_string()), timezone: Some("America/Los_Angeles".to_string()), }; assert_eq!(expected, base.merge(&overlay)); } #[test] fn web_search_tool_config_merge_prefers_overlay_values() { let base = WebSearchToolConfig { context_size: Some(WebSearchContextSize::Low), allowed_domains: Some(vec!["openai.com".to_string()]), location: Some(WebSearchLocation { country: Some("US".to_string()), region: Some("CA".to_string()), city: None, timezone: Some("America/Los_Angeles".to_string()), }), }; let overlay = WebSearchToolConfig { context_size: Some(WebSearchContextSize::High), allowed_domains: None, location: Some(WebSearchLocation { country: None, region: Some("WA".to_string()), city: Some("Seattle".to_string()), timezone: None, }), }; let expected = WebSearchToolConfig { context_size: Some(WebSearchContextSize::High), allowed_domains: Some(vec!["openai.com".to_string()]), location: Some(WebSearchLocation { country: Some("US".to_string()), region: Some("WA".to_string()), city: Some("Seattle".to_string()), timezone: Some("America/Los_Angeles".to_string()), }), }; assert_eq!(expected, base.merge(&overlay)); }}