1use anyhow::Result;
4use clap::{Parser, Subcommand};
5
6pub mod ai;
7pub mod atlassian;
8pub mod browser;
9pub mod commands;
10pub mod completions;
11pub mod config;
12pub mod coverage;
13#[cfg(unix)]
17pub mod daemon;
18pub mod datadog;
19pub mod format;
20pub mod git;
21pub mod help;
22pub mod log;
23pub mod resources;
24#[cfg(unix)]
25pub mod snowflake;
26pub mod transcript;
27#[cfg(unix)]
28pub mod worktrees;
29
30pub use crate::claude::backend::AiBackend;
33
34#[derive(Parser)]
43#[command(name = "omni-dev")]
44#[command(
45 about = "AI-powered git commit rewriter, PR generator, and MCP server for Jira, Confluence, and Datadog.",
46 long_about = None
47)]
48#[command(version)]
49pub struct Cli {
50 #[arg(long, global = true, value_enum)]
56 pub ai_backend: Option<AiBackend>,
57
58 #[arg(long, global = true, value_name = "MODEL")]
65 pub model: Option<String>,
66
67 #[arg(long, global = true, value_name = "KEY:VALUE")]
73 pub beta_header: Option<String>,
74
75 #[arg(long, global = true)]
90 pub claude_cli_allow_tools: bool,
91
92 #[arg(long, global = true)]
104 pub claude_cli_allow_mcp: bool,
105
106 #[arg(long, global = true, value_name = "AMOUNT")]
114 pub claude_cli_max_budget_usd: Option<f64>,
115
116 #[arg(long, global = true, value_name = "PATH")]
121 pub models_yaml: Option<std::path::PathBuf>,
122
123 #[arg(long, global = true, value_name = "NAME")]
131 pub profile: Option<String>,
132
133 #[arg(long = "repo", short = 'C', global = true, value_name = "PATH")]
141 pub repo: Option<std::path::PathBuf>,
142
143 #[command(subcommand)]
145 pub command: Commands,
146}
147
148#[derive(Subcommand)]
154pub enum Commands {
155 Ai(ai::AiCommand),
157 Git(git::GitCommand),
159 Commands(commands::CommandsCommand),
161 Config(config::ConfigCommand),
163 Atlassian(atlassian::AtlassianCommand),
165 Browser(browser::BrowserCommand),
167 #[cfg(unix)]
169 Daemon(daemon::DaemonCommand),
170 Datadog(datadog::DatadogCommand),
172 #[cfg(unix)]
174 Snowflake(snowflake::SnowflakeCommand),
175 #[cfg(unix)]
177 Worktrees(worktrees::WorktreesCommand),
178 Coverage(coverage::CoverageCommand),
180 Transcript(transcript::TranscriptCommand),
182 Log(log::LogCommand),
184 Resources(resources::ResourcesCommand),
186 #[command(hide = true)]
188 Completions(completions::CompletionsCommand),
189 #[command(name = "help-all")]
191 HelpAll(help::HelpCommand),
192}
193
194impl Cli {
195 fn propagate_global_flags(&self) {
200 if let Some(backend) = self.ai_backend {
204 std::env::set_var(crate::claude::backend::AI_BACKEND_ENV, backend.env_value());
205 }
206
207 if let Some(model) = &self.model {
208 std::env::set_var(crate::claude::backend::MODEL_ENV, model);
209 }
210
211 if let Some(beta_header) = &self.beta_header {
212 std::env::set_var(crate::claude::backend::BETA_HEADER_ENV, beta_header);
213 }
214
215 if self.claude_cli_allow_tools {
219 std::env::set_var("OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS", "true");
220 crate::utils::settings::note_cli_flag_export("OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS");
221 }
222
223 if self.claude_cli_allow_mcp {
224 std::env::set_var("OMNI_DEV_CLAUDE_CLI_ALLOW_MCP", "true");
225 crate::utils::settings::note_cli_flag_export("OMNI_DEV_CLAUDE_CLI_ALLOW_MCP");
226 }
227
228 if let Some(budget) = self.claude_cli_max_budget_usd {
229 std::env::set_var("OMNI_DEV_CLAUDE_CLI_MAX_BUDGET_USD", format!("{budget}"));
230 }
231
232 if let Some(path) = &self.models_yaml {
233 std::env::set_var("OMNI_DEV_MODELS_YAML", path);
234 }
235
236 if let Some(profile) = &self.profile {
241 std::env::set_var(crate::utils::settings::PROFILE_ENV_VAR, profile);
242 }
243 }
244
245 fn validate_active_profile<E, F>(env: &E, load_settings: F) -> Result<()>
251 where
252 E: crate::utils::env::EnvSource,
253 F: FnOnce() -> crate::utils::settings::Settings,
254 {
255 match crate::utils::settings::active_profile_from(env) {
256 Some(name) => load_settings().validate_profile(&name),
257 None => Ok(()),
258 }
259 }
260
261 fn load_settings_or_default() -> crate::utils::settings::Settings {
267 crate::utils::settings::Settings::load().unwrap_or_default()
268 }
269
270 pub async fn execute(self) -> Result<()> {
272 self.propagate_global_flags();
273
274 Self::validate_active_profile(
279 &crate::utils::env::SystemEnv,
280 Self::load_settings_or_default,
281 )?;
282
283 let Self { repo, command, .. } = self;
286 let repo = repo.as_deref();
287
288 match command {
289 Commands::Ai(ai_cmd) => ai_cmd.execute().await,
290 Commands::Git(git_cmd) => git_cmd.execute(repo).await,
291 Commands::Commands(commands_cmd) => commands_cmd.execute(),
292 Commands::Atlassian(cmd) => cmd.execute().await,
293 Commands::Browser(cmd) => cmd.execute().await,
294 #[cfg(unix)]
295 Commands::Daemon(cmd) => cmd.execute().await,
296 Commands::Datadog(cmd) => cmd.execute().await,
297 #[cfg(unix)]
298 Commands::Snowflake(cmd) => cmd.execute().await,
299 #[cfg(unix)]
300 Commands::Worktrees(cmd) => cmd.execute().await,
301 Commands::Coverage(cmd) => cmd.execute(repo).await,
302 Commands::Transcript(cmd) => cmd.execute().await,
303 Commands::Log(log_cmd) => log_cmd.execute(),
304 Commands::Config(config_cmd) => config_cmd.execute(),
305 Commands::Resources(resources_cmd) => resources_cmd.execute(),
306 Commands::Completions(completions_cmd) => completions_cmd.execute(),
307 Commands::HelpAll(help_cmd) => help_cmd.execute(),
308 }
309 }
310}
311
312#[cfg(all(target_os = "macos", feature = "menu-bar"))]
313impl Cli {
314 pub fn menu_bar_run_config(&self) -> Option<Result<crate::daemon::DaemonRunConfig>> {
319 match &self.command {
320 Commands::Daemon(daemon::DaemonCommand {
321 command: daemon::DaemonSubcommands::Run(run),
322 }) if !run.no_menu => Some(run.clone().into_run_config()),
323 _ => None,
324 }
325 }
326}
327
328#[cfg(test)]
329#[allow(clippy::unwrap_used, clippy::expect_used)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn parses_ai_backend_claude_cli() {
335 let cli =
336 Cli::try_parse_from(["omni-dev", "--ai-backend", "claude-cli", "help-all"]).unwrap();
337 assert!(matches!(cli.ai_backend, Some(AiBackend::ClaudeCli)));
338 assert!(!cli.claude_cli_allow_tools);
339 }
340
341 #[test]
342 fn parses_ai_backend_default() {
343 let cli = Cli::try_parse_from(["omni-dev", "--ai-backend", "default", "help-all"]).unwrap();
344 assert!(matches!(cli.ai_backend, Some(AiBackend::Default)));
345 }
346
347 #[test]
348 fn parses_ai_backend_openai_ollama_bedrock() {
349 for (value, expected) in [
350 ("openai", AiBackend::OpenAi),
351 ("ollama", AiBackend::Ollama),
352 ("bedrock", AiBackend::Bedrock),
353 ] {
354 let cli = Cli::try_parse_from(["omni-dev", "--ai-backend", value, "help-all"]).unwrap();
355 assert_eq!(cli.ai_backend, Some(expected), "value {value}");
356 }
357 }
358
359 #[test]
360 fn parses_model_before_and_after_subcommand() {
361 let before = Cli::try_parse_from([
365 "omni-dev",
366 "--model",
367 "claude-opus-4-6",
368 "git",
369 "commit",
370 "message",
371 "twiddle",
372 ])
373 .unwrap();
374 assert_eq!(before.model.as_deref(), Some("claude-opus-4-6"));
375
376 let after = Cli::try_parse_from([
379 "omni-dev",
380 "git",
381 "commit",
382 "message",
383 "twiddle",
384 "--model",
385 "claude-opus-4-6",
386 ])
387 .unwrap();
388 assert_eq!(after.model.as_deref(), Some("claude-opus-4-6"));
389 }
390
391 #[test]
392 fn parses_beta_header_before_and_after_subcommand() {
393 let before = Cli::try_parse_from([
394 "omni-dev",
395 "--beta-header",
396 "anthropic-beta:output-128k-2025-02-19",
397 "git",
398 "commit",
399 "message",
400 "check",
401 ])
402 .unwrap();
403 assert_eq!(
404 before.beta_header.as_deref(),
405 Some("anthropic-beta:output-128k-2025-02-19")
406 );
407
408 let after = Cli::try_parse_from([
409 "omni-dev",
410 "git",
411 "commit",
412 "message",
413 "check",
414 "--beta-header",
415 "anthropic-beta:output-128k-2025-02-19",
416 ])
417 .unwrap();
418 assert_eq!(
419 after.beta_header.as_deref(),
420 Some("anthropic-beta:output-128k-2025-02-19")
421 );
422 }
423
424 #[test]
425 fn parses_ai_backend_absent() {
426 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
427 assert!(cli.ai_backend.is_none());
428 assert!(!cli.claude_cli_allow_tools);
429 assert!(!cli.claude_cli_allow_mcp);
430 }
431
432 #[test]
433 fn parses_claude_cli_allow_tools_flag() {
434 let cli =
435 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-tools", "help-all"]).unwrap();
436 assert!(cli.claude_cli_allow_tools);
437 }
438
439 #[test]
440 fn parses_claude_cli_allow_mcp_flag() {
441 let cli = Cli::try_parse_from(["omni-dev", "--claude-cli-allow-mcp", "help-all"]).unwrap();
442 assert!(cli.claude_cli_allow_mcp);
443 assert!(!cli.claude_cli_allow_tools);
444 }
445
446 #[test]
447 fn allow_mcp_and_allow_tools_are_independent() {
448 let only_mcp =
449 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-mcp", "help-all"]).unwrap();
450 assert!(only_mcp.claude_cli_allow_mcp);
451 assert!(!only_mcp.claude_cli_allow_tools);
452
453 let only_tools =
454 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-tools", "help-all"]).unwrap();
455 assert!(only_tools.claude_cli_allow_tools);
456 assert!(!only_tools.claude_cli_allow_mcp);
457
458 let both = Cli::try_parse_from([
459 "omni-dev",
460 "--claude-cli-allow-tools",
461 "--claude-cli-allow-mcp",
462 "help-all",
463 ])
464 .unwrap();
465 assert!(both.claude_cli_allow_tools);
466 assert!(both.claude_cli_allow_mcp);
467 }
468
469 #[test]
470 fn global_flags_accepted_after_subcommand() {
471 let cli = Cli::try_parse_from([
473 "omni-dev",
474 "help-all",
475 "--ai-backend",
476 "claude-cli",
477 "--claude-cli-allow-tools",
478 ])
479 .unwrap();
480 assert!(matches!(cli.ai_backend, Some(AiBackend::ClaudeCli)));
481 assert!(cli.claude_cli_allow_tools);
482 }
483
484 #[test]
485 fn parses_max_budget_usd_flag() {
486 let cli = Cli::try_parse_from([
487 "omni-dev",
488 "--claude-cli-max-budget-usd",
489 "0.50",
490 "help-all",
491 ])
492 .unwrap();
493 assert_eq!(cli.claude_cli_max_budget_usd, Some(0.50));
494 }
495
496 #[test]
497 fn max_budget_usd_absent_is_none() {
498 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
499 assert!(cli.claude_cli_max_budget_usd.is_none());
500 }
501
502 #[test]
503 fn max_budget_usd_rejects_non_numeric() {
504 let result = Cli::try_parse_from([
505 "omni-dev",
506 "--claude-cli-max-budget-usd",
507 "cheap",
508 "help-all",
509 ]);
510 let Err(err) = result else {
511 panic!("expected parse error for non-numeric budget");
512 };
513 assert!(err.to_string().contains("invalid"));
514 }
515
516 const BACKEND_VAR: &str = "OMNI_DEV_AI_BACKEND";
523 const MODEL_VAR: &str = "OMNI_DEV_MODEL";
524 const BETA_HEADER_VAR: &str = "OMNI_DEV_BETA_HEADER";
525 const ALLOW_TOOLS_VAR: &str = "OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS";
526 const ALLOW_MCP_VAR: &str = "OMNI_DEV_CLAUDE_CLI_ALLOW_MCP";
527 const MAX_BUDGET_VAR: &str = "OMNI_DEV_CLAUDE_CLI_MAX_BUDGET_USD";
528 const MODELS_YAML_VAR: &str = "OMNI_DEV_MODELS_YAML";
529 const PROFILE_VAR: &str = "OMNI_DEV_PROFILE";
530
531 struct GlobalFlagsEnvGuard {
534 _lock: std::sync::MutexGuard<'static, ()>,
535 saved: [(&'static str, Option<String>); 8],
536 }
537
538 impl GlobalFlagsEnvGuard {
539 fn new() -> Self {
540 let lock = crate::claude::ai::claude_cli::CLI_ENV_LOCK
541 .lock()
542 .unwrap_or_else(std::sync::PoisonError::into_inner);
543 let names = [
544 BACKEND_VAR,
545 MODEL_VAR,
546 BETA_HEADER_VAR,
547 ALLOW_TOOLS_VAR,
548 ALLOW_MCP_VAR,
549 MAX_BUDGET_VAR,
550 MODELS_YAML_VAR,
551 PROFILE_VAR,
552 ];
553 let saved = names.map(|n| (n, std::env::var(n).ok()));
554 for (n, _) in &saved {
555 std::env::remove_var(n);
556 }
557 Self { _lock: lock, saved }
558 }
559 }
560
561 impl Drop for GlobalFlagsEnvGuard {
562 fn drop(&mut self) {
563 for (n, value) in &self.saved {
564 match value {
565 Some(v) => std::env::set_var(n, v),
566 None => std::env::remove_var(n),
567 }
568 }
569 }
570 }
571
572 fn cli_with_defaults() -> Cli {
573 Cli::try_parse_from(["omni-dev", "help-all"]).unwrap()
574 }
575
576 #[test]
577 fn propagate_global_flags_defaults_set_nothing() {
578 let _g = GlobalFlagsEnvGuard::new();
579 cli_with_defaults().propagate_global_flags();
580 assert!(std::env::var(BACKEND_VAR).is_err());
581 assert!(std::env::var(MODEL_VAR).is_err());
582 assert!(std::env::var(BETA_HEADER_VAR).is_err());
583 assert!(std::env::var(ALLOW_TOOLS_VAR).is_err());
584 assert!(std::env::var(ALLOW_MCP_VAR).is_err());
585 assert!(std::env::var(MAX_BUDGET_VAR).is_err());
586 assert!(std::env::var(MODELS_YAML_VAR).is_err());
587 assert!(std::env::var(PROFILE_VAR).is_err());
588 }
589
590 #[test]
591 fn propagate_global_flags_sets_ai_backend_claude_cli() {
592 let _g = GlobalFlagsEnvGuard::new();
593 let mut cli = cli_with_defaults();
594 cli.ai_backend = Some(AiBackend::ClaudeCli);
595 cli.propagate_global_flags();
596 assert_eq!(
597 std::env::var(BACKEND_VAR).ok().as_deref(),
598 Some("claude-cli")
599 );
600 }
601
602 #[test]
603 fn propagate_global_flags_default_backend_overrides_env_var() {
604 let _g = GlobalFlagsEnvGuard::new();
608 std::env::set_var(BACKEND_VAR, "claude-cli");
609 let mut cli = cli_with_defaults();
610 cli.ai_backend = Some(AiBackend::Default);
611 cli.propagate_global_flags();
612 assert_eq!(std::env::var(BACKEND_VAR).ok().as_deref(), Some("default"));
613 }
614
615 #[test]
616 fn propagate_global_flags_sets_openai_ollama_bedrock() {
617 let _g = GlobalFlagsEnvGuard::new();
618 for (backend, expected) in [
619 (AiBackend::OpenAi, "openai"),
620 (AiBackend::Ollama, "ollama"),
621 (AiBackend::Bedrock, "bedrock"),
622 ] {
623 let mut cli = cli_with_defaults();
624 cli.ai_backend = Some(backend);
625 cli.propagate_global_flags();
626 assert_eq!(std::env::var(BACKEND_VAR).ok().as_deref(), Some(expected));
627 }
628 }
629
630 #[test]
631 fn propagate_global_flags_sets_model() {
632 let _g = GlobalFlagsEnvGuard::new();
633 let mut cli = cli_with_defaults();
634 cli.model = Some("claude-opus-4-6".to_string());
635 cli.propagate_global_flags();
636 assert_eq!(
637 std::env::var(MODEL_VAR).ok().as_deref(),
638 Some("claude-opus-4-6")
639 );
640 }
641
642 #[test]
643 fn propagate_global_flags_sets_beta_header() {
644 let _g = GlobalFlagsEnvGuard::new();
645 let mut cli = cli_with_defaults();
646 cli.beta_header = Some("anthropic-beta:output-128k-2025-02-19".to_string());
647 cli.propagate_global_flags();
648 assert_eq!(
649 std::env::var(BETA_HEADER_VAR).ok().as_deref(),
650 Some("anthropic-beta:output-128k-2025-02-19")
651 );
652 }
653
654 #[test]
655 fn propagate_global_flags_sets_allow_tools() {
656 let _g = GlobalFlagsEnvGuard::new();
657 let mut cli = cli_with_defaults();
658 cli.claude_cli_allow_tools = true;
659 cli.propagate_global_flags();
660 assert_eq!(std::env::var(ALLOW_TOOLS_VAR).ok().as_deref(), Some("true"));
661 assert!(crate::utils::settings::exported_by_cli_flag(
664 ALLOW_TOOLS_VAR
665 ));
666 }
667
668 #[test]
669 fn propagate_global_flags_sets_allow_mcp() {
670 let _g = GlobalFlagsEnvGuard::new();
671 let mut cli = cli_with_defaults();
672 cli.claude_cli_allow_mcp = true;
673 cli.propagate_global_flags();
674 assert_eq!(std::env::var(ALLOW_MCP_VAR).ok().as_deref(), Some("true"));
675 assert!(crate::utils::settings::exported_by_cli_flag(ALLOW_MCP_VAR));
676 }
677
678 #[test]
679 fn propagate_global_flags_sets_max_budget_usd() {
680 let _g = GlobalFlagsEnvGuard::new();
681 let mut cli = cli_with_defaults();
682 cli.claude_cli_max_budget_usd = Some(1.5);
683 cli.propagate_global_flags();
684 assert_eq!(std::env::var(MAX_BUDGET_VAR).ok().as_deref(), Some("1.5"));
685 }
686
687 #[test]
688 fn parses_models_yaml_flag() {
689 let cli = Cli::try_parse_from([
690 "omni-dev",
691 "--models-yaml",
692 "/tmp/custom-models.yaml",
693 "help-all",
694 ])
695 .unwrap();
696 assert_eq!(
697 cli.models_yaml.as_deref(),
698 Some(std::path::Path::new("/tmp/custom-models.yaml"))
699 );
700 }
701
702 #[test]
703 fn parses_repo_flag_long_and_short() {
704 let long = Cli::try_parse_from(["omni-dev", "--repo", "/tmp/r", "help-all"]).unwrap();
705 assert_eq!(
706 long.repo.as_deref(),
707 Some(std::path::Path::new("/tmp/r")),
708 "--repo should populate cli.repo"
709 );
710 let short = Cli::try_parse_from(["omni-dev", "-C", "/tmp/r", "help-all"]).unwrap();
711 assert_eq!(
712 short.repo.as_deref(),
713 Some(std::path::Path::new("/tmp/r")),
714 "-C should populate cli.repo"
715 );
716 let absent = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
717 assert!(absent.repo.is_none());
718 }
719
720 #[test]
723 fn repo_flag_is_not_propagated_to_env() {
724 let _g = GlobalFlagsEnvGuard::new();
725 let mut cli = cli_with_defaults();
726 cli.repo = Some(std::path::PathBuf::from("/tmp/some-repo"));
727 cli.propagate_global_flags();
728 assert!(
729 std::env::var("OMNI_DEV_REPO").is_err(),
730 "repo must not be exported to an env var"
731 );
732 }
733
734 #[test]
735 fn propagate_global_flags_sets_models_yaml() {
736 let _g = GlobalFlagsEnvGuard::new();
737 let mut cli = cli_with_defaults();
738 cli.models_yaml = Some(std::path::PathBuf::from("/tmp/custom-models.yaml"));
739 cli.propagate_global_flags();
740 assert_eq!(
741 std::env::var(MODELS_YAML_VAR).ok().as_deref(),
742 Some("/tmp/custom-models.yaml")
743 );
744 }
745
746 #[test]
747 fn parses_profile_flag() {
748 let cli = Cli::try_parse_from(["omni-dev", "--profile", "work", "help-all"]).unwrap();
749 assert_eq!(cli.profile.as_deref(), Some("work"));
750 }
751
752 #[test]
753 fn profile_absent_is_none() {
754 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
755 assert!(cli.profile.is_none());
756 }
757
758 #[test]
759 fn propagate_global_flags_sets_profile() {
760 let _g = GlobalFlagsEnvGuard::new();
761 let mut cli = cli_with_defaults();
762 cli.profile = Some("work".to_string());
763 cli.propagate_global_flags();
764 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("work"));
765 }
766
767 #[test]
768 fn propagate_global_flags_profile_flag_beats_env_var() {
769 let _g = GlobalFlagsEnvGuard::new();
770 std::env::set_var(PROFILE_VAR, "personal");
771 let mut cli = cli_with_defaults();
772 cli.profile = Some("work".to_string());
773 cli.propagate_global_flags();
774 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("work"));
775 }
776
777 #[test]
778 fn propagate_global_flags_absent_profile_leaves_env_var() {
779 let _g = GlobalFlagsEnvGuard::new();
780 std::env::set_var(PROFILE_VAR, "personal");
781 cli_with_defaults().propagate_global_flags();
782 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("personal"));
783 }
784
785 #[test]
789 fn validate_active_profile_ok_and_skips_load_when_no_profile() {
790 use crate::test_support::env::MapEnv;
791 let env = MapEnv::new();
792 let result = Cli::validate_active_profile(&env, || panic!("must not load settings"));
793 assert!(result.is_ok());
794 }
795
796 #[test]
797 fn validate_active_profile_ok_for_known_profile() {
798 use crate::test_support::env::MapEnv;
799 use crate::utils::settings::{Profile, Settings};
800 let env = MapEnv::new().with(PROFILE_VAR, "work");
801 let settings = Settings {
802 profiles: std::iter::once(("work".to_string(), Profile::default())).collect(),
803 ..Default::default()
804 };
805 assert!(Cli::validate_active_profile(&env, || settings).is_ok());
806 }
807
808 #[test]
809 fn validate_active_profile_errors_for_unknown_profile() {
810 use crate::test_support::env::MapEnv;
811 use crate::utils::settings::Settings;
812 let env = MapEnv::new().with(PROFILE_VAR, "wrok");
813 let err = Cli::validate_active_profile(&env, Settings::default)
814 .unwrap_err()
815 .to_string();
816 assert!(err.contains("unknown profile 'wrok'"));
817 }
818
819 #[test]
820 fn load_settings_or_default_never_panics() {
821 let _settings = Cli::load_settings_or_default();
825 }
826
827 #[test]
828 fn propagate_global_flags_independent_flags_compose() {
829 let _g = GlobalFlagsEnvGuard::new();
830 let mut cli = cli_with_defaults();
831 cli.ai_backend = Some(AiBackend::ClaudeCli);
832 cli.claude_cli_allow_tools = true;
833 cli.claude_cli_allow_mcp = true;
834 cli.claude_cli_max_budget_usd = Some(0.25);
835 cli.propagate_global_flags();
836 assert_eq!(
837 std::env::var(BACKEND_VAR).ok().as_deref(),
838 Some("claude-cli")
839 );
840 assert_eq!(std::env::var(ALLOW_TOOLS_VAR).ok().as_deref(), Some("true"));
841 assert_eq!(std::env::var(ALLOW_MCP_VAR).ok().as_deref(), Some("true"));
842 assert_eq!(std::env::var(MAX_BUDGET_VAR).ok().as_deref(), Some("0.25"));
843 }
844}