1use codex_utils_absolute_path::AbsolutePathBuf;
2use schemars::JsonSchema;
3use schemars::r#gen::SchemaGenerator;
4use schemars::schema::InstanceType;
5use schemars::schema::Metadata;
6use schemars::schema::Schema;
7use schemars::schema::SchemaObject;
8use serde::Deserialize;
9use serde::Serialize;
10use serde_json::Value;
11use std::collections::HashMap;
12use std::fmt;
13use std::num::NonZeroU64;
14use std::ops::Deref;
15use std::str::FromStr;
16use std::time::Duration;
17use strum_macros::Display;
18use strum_macros::EnumIter;
19use ts_rs::TS;
20use wildmatch::WildMatchPattern;
21
22use crate::openai_models::ReasoningEffort;
23
24#[derive(
27 Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
28)]
29#[serde(rename_all = "snake_case")]
30#[strum(serialize_all = "snake_case")]
31pub enum AutoCompactTokenLimitScope {
32 #[default]
34 Total,
35 BodyAfterPrefix,
37}
38
39#[derive(
43 Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
44)]
45#[serde(rename_all = "lowercase")]
46#[strum(serialize_all = "lowercase")]
47pub enum ReasoningSummary {
48 #[default]
49 Auto,
50 Concise,
51 Detailed,
52 None,
54}
55
56#[derive(
59 Hash,
60 Debug,
61 Serialize,
62 Deserialize,
63 Default,
64 Clone,
65 Copy,
66 PartialEq,
67 Eq,
68 Display,
69 JsonSchema,
70 TS,
71)]
72#[serde(rename_all = "lowercase")]
73#[strum(serialize_all = "lowercase")]
74pub enum Verbosity {
75 Low,
76 #[default]
77 Medium,
78 High,
79}
80
81#[derive(
82 Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display, JsonSchema, TS,
83)]
84#[serde(rename_all = "kebab-case")]
85#[strum(serialize_all = "kebab-case")]
86pub enum SandboxMode {
87 #[serde(rename = "read-only")]
88 #[default]
89 ReadOnly,
90
91 #[serde(rename = "workspace-write")]
92 WorkspaceWrite,
93
94 #[serde(rename = "danger-full-access")]
95 DangerFullAccess,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct ProfileV2Name(String);
101
102impl ProfileV2Name {
103 pub fn as_str(&self) -> &str {
104 &self.0
105 }
106}
107
108#[derive(Debug, PartialEq, Eq)]
109pub struct ProfileV2NameParseError {
110 value: String,
111}
112
113impl fmt::Display for ProfileV2NameParseError {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(
116 f,
117 "invalid --profile value `{}`; pass a plain name such as `work`",
118 self.value
119 )
120 }
121}
122
123impl std::error::Error for ProfileV2NameParseError {}
124
125impl FromStr for ProfileV2Name {
126 type Err = ProfileV2NameParseError;
127
128 fn from_str(value: &str) -> Result<Self, Self::Err> {
129 if value.is_empty()
130 || !value
131 .bytes()
132 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
133 {
134 return Err(ProfileV2NameParseError {
135 value: value.to_string(),
136 });
137 }
138
139 Ok(Self(value.to_string()))
140 }
141}
142
143impl Deref for ProfileV2Name {
144 type Target = str;
145
146 fn deref(&self) -> &Self::Target {
147 self.as_str()
148 }
149}
150
151impl fmt::Display for ProfileV2Name {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 self.0.fmt(f)
154 }
155}
156
157#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, TS)]
158#[strum(serialize_all = "snake_case")]
159#[ts(type = r#""user" | "auto_review" | "guardian_subagent""#)]
160pub enum ApprovalsReviewer {
166 #[default]
167 #[serde(rename = "user")]
168 User,
169 #[serde(rename = "auto_review", alias = "guardian_subagent")]
170 #[strum(serialize = "auto_review")]
171 AutoReview,
172}
173
174impl JsonSchema for ApprovalsReviewer {
175 fn schema_name() -> String {
176 "ApprovalsReviewer".to_string()
177 }
178
179 fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
180 string_enum_schema_with_description(
181 &["user", "auto_review", "guardian_subagent"],
182 "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.",
183 )
184 }
185}
186
187#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
188#[serde(rename_all = "kebab-case")]
189pub enum ShellEnvironmentPolicyInherit {
190 Core,
193
194 #[default]
196 All,
197
198 None,
200}
201
202#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
205#[serde(rename_all = "lowercase")]
206#[ts(export_to = "v2/")]
207pub enum ShellEnvironmentPolicyFilter {
208 Include,
209 Exclude,
210}
211
212pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
213
214#[derive(Debug, Clone, PartialEq)]
222pub struct ShellEnvironmentPolicy {
223 pub inherit: ShellEnvironmentPolicyInherit,
225
226 pub ignore_default_excludes: bool,
229
230 pub exclude: Vec<EnvironmentVariablePattern>,
232
233 pub r#set: HashMap<String, String>,
235
236 pub include_only: Vec<EnvironmentVariablePattern>,
238
239 pub use_profile: bool,
241}
242
243impl Default for ShellEnvironmentPolicy {
244 fn default() -> Self {
245 Self {
246 inherit: ShellEnvironmentPolicyInherit::All,
247 ignore_default_excludes: true,
248 exclude: Vec::new(),
249 r#set: HashMap::new(),
250 include_only: Vec::new(),
251 use_profile: false,
252 }
253 }
254}
255
256fn string_enum_schema_with_description(values: &[&str], description: &str) -> Schema {
257 let mut schema = SchemaObject {
258 instance_type: Some(InstanceType::String.into()),
259 metadata: Some(Box::new(Metadata {
260 description: Some(description.to_string()),
261 ..Default::default()
262 })),
263 ..Default::default()
264 };
265 schema.enum_values = Some(
266 values
267 .iter()
268 .map(|value| Value::String((*value).to_string()))
269 .collect(),
270 );
271 Schema::Object(schema)
272}
273
274#[derive(
275 Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS,
276)]
277#[serde(rename_all = "kebab-case")]
278#[strum(serialize_all = "kebab-case")]
279pub enum WindowsSandboxLevel {
280 #[default]
281 Disabled,
282 RestrictedToken,
283 Elevated,
284}
285
286#[derive(
287 Debug,
288 Serialize,
289 Deserialize,
290 Clone,
291 Copy,
292 PartialEq,
293 Eq,
294 Display,
295 JsonSchema,
296 TS,
297 PartialOrd,
298 Ord,
299 EnumIter,
300)]
301#[serde(rename_all = "lowercase")]
302#[strum(serialize_all = "lowercase")]
303pub enum Personality {
304 None,
305 Friendly,
306 Pragmatic,
307}
308
309#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Display, JsonSchema, TS, Default)]
312#[serde(rename_all = "camelCase", from = "MultiAgentModeWire")]
313#[ts(rename_all = "camelCase")]
314#[strum(serialize_all = "camelCase")]
315pub enum MultiAgentMode {
316 Custom(String),
317 #[default]
318 ExplicitRequestOnly,
319 Proactive,
320}
321
322#[derive(Deserialize)]
323#[serde(rename_all = "camelCase")]
324enum MultiAgentModeWire {
325 None,
326 Custom(String),
327 ExplicitRequestOnly,
328 Proactive,
329}
330
331impl From<MultiAgentModeWire> for MultiAgentMode {
332 fn from(value: MultiAgentModeWire) -> Self {
333 match value {
334 MultiAgentModeWire::None => Self::Custom(String::new()),
335 MultiAgentModeWire::Custom(hint_text) => Self::Custom(hint_text),
336 MultiAgentModeWire::ExplicitRequestOnly => Self::ExplicitRequestOnly,
337 MultiAgentModeWire::Proactive => Self::Proactive,
338 }
339 }
340}
341
342#[derive(
343 Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
344)]
345#[serde(rename_all = "snake_case")]
346#[strum(serialize_all = "snake_case")]
347pub enum WebSearchMode {
348 Disabled,
349 #[default]
350 Cached,
351 Indexed,
352 Live,
353}
354
355#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
356#[serde(rename_all = "lowercase")]
357#[strum(serialize_all = "lowercase")]
358pub enum WebSearchContextSize {
359 Low,
360 Medium,
361 High,
362}
363
364#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
365#[schemars(deny_unknown_fields)]
366pub struct WebSearchLocation {
367 pub country: Option<String>,
368 pub region: Option<String>,
369 pub city: Option<String>,
370 pub timezone: Option<String>,
371}
372
373impl WebSearchLocation {
374 pub fn merge(&self, other: &Self) -> Self {
375 Self {
376 country: other.country.clone().or_else(|| self.country.clone()),
377 region: other.region.clone().or_else(|| self.region.clone()),
378 city: other.city.clone().or_else(|| self.city.clone()),
379 timezone: other.timezone.clone().or_else(|| self.timezone.clone()),
380 }
381 }
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
385#[schemars(deny_unknown_fields)]
386pub struct WebSearchToolConfig {
387 pub context_size: Option<WebSearchContextSize>,
388 pub allowed_domains: Option<Vec<String>>,
389 pub location: Option<WebSearchLocation>,
390}
391
392impl WebSearchToolConfig {
393 pub fn merge(&self, other: &Self) -> Self {
394 Self {
395 context_size: other.context_size.or(self.context_size),
396 allowed_domains: other
397 .allowed_domains
398 .clone()
399 .or_else(|| self.allowed_domains.clone()),
400 location: match (&self.location, &other.location) {
401 (Some(location), Some(other_location)) => Some(location.merge(other_location)),
402 (Some(location), None) => Some(location.clone()),
403 (None, Some(other_location)) => Some(other_location.clone()),
404 (None, None) => None,
405 },
406 }
407 }
408}
409
410#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
411#[schemars(deny_unknown_fields)]
412pub struct WebSearchFilters {
413 pub allowed_domains: Option<Vec<String>>,
414}
415
416#[derive(
417 Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Display, JsonSchema, TS,
418)]
419#[serde(rename_all = "lowercase")]
420#[strum(serialize_all = "lowercase")]
421pub enum WebSearchUserLocationType {
422 #[default]
423 Approximate,
424}
425
426#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
427#[schemars(deny_unknown_fields)]
428pub struct WebSearchUserLocation {
429 #[serde(default)]
430 pub r#type: WebSearchUserLocationType,
431 pub country: Option<String>,
432 pub region: Option<String>,
433 pub city: Option<String>,
434 pub timezone: Option<String>,
435}
436
437#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
438#[schemars(deny_unknown_fields)]
439pub struct WebSearchConfig {
440 pub filters: Option<WebSearchFilters>,
441 pub user_location: Option<WebSearchUserLocation>,
442 pub search_context_size: Option<WebSearchContextSize>,
443}
444
445impl From<WebSearchLocation> for WebSearchUserLocation {
446 fn from(location: WebSearchLocation) -> Self {
447 Self {
448 r#type: WebSearchUserLocationType::Approximate,
449 country: location.country,
450 region: location.region,
451 city: location.city,
452 timezone: location.timezone,
453 }
454 }
455}
456
457impl From<WebSearchToolConfig> for WebSearchConfig {
458 fn from(config: WebSearchToolConfig) -> Self {
459 Self {
460 filters: config
461 .allowed_domains
462 .map(|allowed_domains| WebSearchFilters {
463 allowed_domains: Some(allowed_domains),
464 }),
465 user_location: config.location.map(Into::into),
466 search_context_size: config.context_size,
467 }
468 }
469}
470
471#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
472#[serde(rename_all = "lowercase")]
473#[strum(serialize_all = "lowercase")]
474pub enum ServiceTier {
475 Fast,
476 Flex,
477}
478
479pub const SERVICE_TIER_DEFAULT_REQUEST_VALUE: &str = "default";
484
485impl ServiceTier {
486 pub const fn request_value(self) -> &'static str {
487 match self {
488 Self::Fast => "priority",
489 Self::Flex => "flex",
490 }
491 }
492
493 pub fn from_request_value(value: &str) -> Option<Self> {
494 match value {
495 "fast" | "priority" => Some(Self::Fast),
496 "flex" => Some(Self::Flex),
497 _ => None,
498 }
499 }
500}
501
502#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
503#[serde(rename_all = "lowercase")]
504#[strum(serialize_all = "lowercase")]
505pub enum ForcedLoginMethod {
506 Chatgpt,
507 Api,
508}
509
510const DEFAULT_PROVIDER_AUTH_TIMEOUT_MS: u64 = 5_000;
511const DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS: u64 = 300_000;
512
513#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
515#[schemars(deny_unknown_fields)]
516pub struct ModelProviderAuthInfo {
517 pub command: String,
519
520 #[serde(default)]
522 pub args: Vec<String>,
523
524 #[serde(default = "default_provider_auth_timeout_ms")]
526 pub timeout_ms: NonZeroU64,
527
528 #[serde(default = "default_provider_auth_refresh_interval_ms")]
531 pub refresh_interval_ms: u64,
532
533 #[serde(default = "default_provider_auth_cwd")]
535 #[schemars(skip_serializing_if = "is_default_provider_auth_cwd")]
536 pub cwd: AbsolutePathBuf,
537}
538
539impl ModelProviderAuthInfo {
540 pub fn timeout(&self) -> Duration {
541 Duration::from_millis(self.timeout_ms.get())
542 }
543
544 pub fn refresh_interval(&self) -> Option<Duration> {
545 NonZeroU64::new(self.refresh_interval_ms).map(|value| Duration::from_millis(value.get()))
546 }
547}
548
549fn default_provider_auth_timeout_ms() -> NonZeroU64 {
550 non_zero_u64(
551 DEFAULT_PROVIDER_AUTH_TIMEOUT_MS,
552 "model_providers.<id>.auth.timeout_ms",
553 )
554}
555
556fn default_provider_auth_refresh_interval_ms() -> u64 {
557 DEFAULT_PROVIDER_AUTH_REFRESH_INTERVAL_MS
558}
559
560fn non_zero_u64(value: u64, field_name: &str) -> NonZeroU64 {
561 match NonZeroU64::new(value) {
562 Some(value) => value,
563 None => panic!("{field_name} must be non-zero"),
564 }
565}
566
567fn default_provider_auth_cwd() -> AbsolutePathBuf {
568 let deserializer = serde::de::value::StrDeserializer::<serde::de::value::Error>::new(".");
569 if let Ok(cwd) = AbsolutePathBuf::deserialize(deserializer) {
570 return cwd;
571 }
572
573 match AbsolutePathBuf::current_dir() {
574 Ok(cwd) => cwd,
575 Err(err) => panic!("provider auth cwd must resolve: {err}"),
576 }
577}
578
579fn is_default_provider_auth_cwd(path: &AbsolutePathBuf) -> bool {
580 path == &default_provider_auth_cwd()
581}
582
583#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
586#[serde(rename_all = "lowercase")]
587#[strum(serialize_all = "lowercase")]
588pub enum TrustLevel {
589 Trusted,
590 Untrusted,
591}
592
593#[derive(
601 Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
602)]
603#[serde(rename_all = "lowercase")]
604#[strum(serialize_all = "lowercase")]
605pub enum AltScreenMode {
606 #[default]
608 Auto,
609 Always,
611 Never,
613}
614
615#[derive(
617 Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, TS, Default,
618)]
619#[serde(rename_all = "snake_case")]
620pub enum ModeKind {
621 Plan,
622 #[default]
623 #[serde(
624 alias = "code",
625 alias = "pair_programming",
626 alias = "execute",
627 alias = "custom"
628 )]
629 Default,
630 #[doc(hidden)]
631 #[serde(skip_serializing, skip_deserializing)]
632 #[schemars(skip)]
633 #[ts(skip)]
634 PairProgramming,
635 #[doc(hidden)]
636 #[serde(skip_serializing, skip_deserializing)]
637 #[schemars(skip)]
638 #[ts(skip)]
639 Execute,
640}
641
642pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];
643
644impl ModeKind {
645 pub const fn display_name(self) -> &'static str {
646 match self {
647 Self::Plan => "Plan",
648 Self::Default => "Default",
649 Self::PairProgramming => "Pair Programming",
650 Self::Execute => "Execute",
651 }
652 }
653
654 pub const fn is_tui_visible(self) -> bool {
655 matches!(self, Self::Plan | Self::Default)
656 }
657
658 pub const fn allows_request_user_input(self) -> bool {
659 matches!(self, Self::Plan)
660 }
661}
662
663#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
665#[serde(rename_all = "lowercase")]
666pub struct CollaborationMode {
667 pub mode: ModeKind,
668 pub settings: Settings,
669}
670
671impl CollaborationMode {
672 fn settings_ref(&self) -> &Settings {
674 &self.settings
675 }
676
677 pub fn model(&self) -> &str {
678 self.settings_ref().model.as_str()
679 }
680
681 pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
682 self.settings_ref().reasoning_effort.clone()
683 }
684
685 pub fn with_updates(
693 &self,
694 model: Option<String>,
695 effort: Option<Option<ReasoningEffort>>,
696 developer_instructions: Option<Option<String>>,
697 ) -> Self {
698 let settings = self.settings_ref();
699 let updated_settings = Settings {
700 model: model.unwrap_or_else(|| settings.model.clone()),
701 reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()),
702 developer_instructions: developer_instructions
703 .unwrap_or_else(|| settings.developer_instructions.clone()),
704 };
705
706 CollaborationMode {
707 mode: self.mode,
708 settings: updated_settings,
709 }
710 }
711
712 pub fn apply_mask(&self, mask: &CollaborationModeMask) -> Self {
718 let settings = self.settings_ref();
719 CollaborationMode {
720 mode: mask.mode.unwrap_or(self.mode),
721 settings: Settings {
722 model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
723 reasoning_effort: mask
724 .reasoning_effort
725 .clone()
726 .unwrap_or_else(|| settings.reasoning_effort.clone()),
727 developer_instructions: mask
728 .developer_instructions
729 .clone()
730 .unwrap_or_else(|| settings.developer_instructions.clone()),
731 },
732 }
733 }
734}
735
736#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
738pub struct Settings {
739 pub model: String,
740 pub reasoning_effort: Option<ReasoningEffort>,
741 pub developer_instructions: Option<String>,
742}
743
744#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
747pub struct CollaborationModeMask {
748 pub name: String,
749 pub mode: Option<ModeKind>,
750 pub model: Option<String>,
751 pub reasoning_effort: Option<Option<ReasoningEffort>>,
752 pub developer_instructions: Option<Option<String>>,
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use pretty_assertions::assert_eq;
759
760 #[test]
761 fn apply_mask_can_clear_optional_fields() {
762 let mode = CollaborationMode {
763 mode: ModeKind::Default,
764 settings: Settings {
765 model: "gpt-5.2-codex".to_string(),
766 reasoning_effort: Some(ReasoningEffort::High),
767 developer_instructions: Some("stay focused".to_string()),
768 },
769 };
770 let mask = CollaborationModeMask {
771 name: "Clear".to_string(),
772 mode: None,
773 model: None,
774 reasoning_effort: Some(None),
775 developer_instructions: Some(None),
776 };
777
778 let expected = CollaborationMode {
779 mode: ModeKind::Default,
780 settings: Settings {
781 model: "gpt-5.2-codex".to_string(),
782 reasoning_effort: None,
783 developer_instructions: None,
784 },
785 };
786 assert_eq!(expected, mode.apply_mask(&mask));
787 }
788
789 #[test]
790 fn mode_kind_deserializes_alias_values_to_default() {
791 for alias in ["code", "pair_programming", "execute", "custom"] {
792 let json = format!("\"{alias}\"");
793 let mode: ModeKind = serde_json::from_str(&json).expect("deserialize mode");
794 assert_eq!(ModeKind::Default, mode);
795 }
796 }
797
798 #[test]
799 fn approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent() {
800 assert_eq!(ApprovalsReviewer::User.to_string(), "user");
801 assert_eq!(
802 serde_json::to_string(&ApprovalsReviewer::User).expect("serialize reviewer"),
803 "\"user\""
804 );
805 assert_eq!(
806 serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"),
807 "\"auto_review\""
808 );
809
810 for value in ["user", "auto_review", "guardian_subagent"] {
811 let json = format!("\"{value}\"");
812 let reviewer: ApprovalsReviewer =
813 serde_json::from_str(&json).expect("deserialize reviewer");
814 let expected = if value == "user" {
815 ApprovalsReviewer::User
816 } else {
817 ApprovalsReviewer::AutoReview
818 };
819 assert_eq!(expected, reviewer);
820 }
821 }
822
823 #[test]
824 fn profile_v2_name_rejects_paths_and_empty_names() {
825 assert_eq!(
826 ProfileV2Name::from_str("../foo"),
827 Err(ProfileV2NameParseError {
828 value: "../foo".to_string(),
829 }),
830 "dots and slashes are disallowed to prevent reading arbitrary files"
831 );
832 assert_eq!(
833 ProfileV2Name::from_str(""),
834 Err(ProfileV2NameParseError {
835 value: String::new(),
836 }),
837 "profile name cannot be empty"
838 );
839 }
840
841 #[test]
842 fn tui_visible_collaboration_modes_match_mode_kind_visibility() {
843 let expected = [ModeKind::Default, ModeKind::Plan];
844 assert_eq!(expected, TUI_VISIBLE_COLLABORATION_MODES);
845
846 for mode in TUI_VISIBLE_COLLABORATION_MODES {
847 assert!(mode.is_tui_visible());
848 }
849
850 assert!(!ModeKind::PairProgramming.is_tui_visible());
851 assert!(!ModeKind::Execute.is_tui_visible());
852 }
853
854 #[test]
855 fn web_search_location_merge_prefers_overlay_values() {
856 let base = WebSearchLocation {
857 country: Some("US".to_string()),
858 region: Some("CA".to_string()),
859 city: None,
860 timezone: Some("America/Los_Angeles".to_string()),
861 };
862 let overlay = WebSearchLocation {
863 country: None,
864 region: Some("WA".to_string()),
865 city: Some("Seattle".to_string()),
866 timezone: None,
867 };
868
869 let expected = WebSearchLocation {
870 country: Some("US".to_string()),
871 region: Some("WA".to_string()),
872 city: Some("Seattle".to_string()),
873 timezone: Some("America/Los_Angeles".to_string()),
874 };
875
876 assert_eq!(expected, base.merge(&overlay));
877 }
878
879 #[test]
880 fn web_search_tool_config_merge_prefers_overlay_values() {
881 let base = WebSearchToolConfig {
882 context_size: Some(WebSearchContextSize::Low),
883 allowed_domains: Some(vec!["openai.com".to_string()]),
884 location: Some(WebSearchLocation {
885 country: Some("US".to_string()),
886 region: Some("CA".to_string()),
887 city: None,
888 timezone: Some("America/Los_Angeles".to_string()),
889 }),
890 };
891 let overlay = WebSearchToolConfig {
892 context_size: Some(WebSearchContextSize::High),
893 allowed_domains: None,
894 location: Some(WebSearchLocation {
895 country: None,
896 region: Some("WA".to_string()),
897 city: Some("Seattle".to_string()),
898 timezone: None,
899 }),
900 };
901
902 let expected = WebSearchToolConfig {
903 context_size: Some(WebSearchContextSize::High),
904 allowed_domains: Some(vec!["openai.com".to_string()]),
905 location: Some(WebSearchLocation {
906 country: Some("US".to_string()),
907 region: Some("WA".to_string()),
908 city: Some("Seattle".to_string()),
909 timezone: Some("America/Los_Angeles".to_string()),
910 }),
911 };
912
913 assert_eq!(expected, base.merge(&overlay));
914 }
915}