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 sessions;
26#[cfg(unix)]
27pub mod snowflake;
28pub mod transcript;
29#[cfg(unix)]
30pub mod worktrees;
31
32pub use crate::claude::backend::AiBackend;
35
36#[derive(Parser)]
45#[command(name = "omni-dev")]
46#[command(
47 about = "AI-powered git commit rewriter, PR generator, and MCP server for Jira, Confluence, and Datadog.",
48 long_about = None
49)]
50#[command(version = crate::VERSION, long_version = crate::build_info::long_version())]
53pub struct Cli {
54 #[arg(long, global = true, value_enum)]
60 pub ai_backend: Option<AiBackend>,
61
62 #[arg(long, global = true, value_name = "MODEL")]
69 pub model: Option<String>,
70
71 #[arg(long, global = true, value_name = "KEY:VALUE")]
77 pub beta_header: Option<String>,
78
79 #[arg(long, global = true)]
94 pub claude_cli_allow_tools: bool,
95
96 #[arg(long, global = true)]
108 pub claude_cli_allow_mcp: bool,
109
110 #[arg(long, global = true, value_name = "AMOUNT")]
118 pub claude_cli_max_budget_usd: Option<f64>,
119
120 #[arg(long, global = true, value_name = "PATH")]
125 pub models_yaml: Option<std::path::PathBuf>,
126
127 #[arg(long, global = true, value_name = "NAME")]
135 pub profile: Option<String>,
136
137 #[arg(long, global = true, value_name = "URL")]
146 pub instance: Option<String>,
147
148 #[arg(long = "repo", short = 'C', global = true, value_name = "PATH")]
156 pub repo: Option<std::path::PathBuf>,
157
158 #[command(subcommand)]
160 pub command: Commands,
161}
162
163#[derive(Subcommand)]
169pub enum Commands {
170 Ai(ai::AiCommand),
172 Git(git::GitCommand),
174 Commands(commands::CommandsCommand),
176 Config(config::ConfigCommand),
178 Atlassian(atlassian::AtlassianCommand),
180 Browser(browser::BrowserCommand),
182 #[cfg(unix)]
184 Daemon(daemon::DaemonCommand),
185 Datadog(datadog::DatadogCommand),
187 #[cfg(unix)]
189 Snowflake(snowflake::SnowflakeCommand),
190 #[cfg(unix)]
192 Worktrees(worktrees::WorktreesCommand),
193 #[cfg(unix)]
195 Sessions(sessions::SessionsCommand),
196 Coverage(coverage::CoverageCommand),
198 Transcript(transcript::TranscriptCommand),
200 Log(log::LogCommand),
202 Resources(resources::ResourcesCommand),
204 #[command(hide = true)]
206 Completions(completions::CompletionsCommand),
207 #[command(name = "help-all")]
209 HelpAll(help::HelpCommand),
210}
211
212impl Cli {
213 fn propagate_global_flags(&self) {
218 if let Some(backend) = self.ai_backend {
222 std::env::set_var(crate::claude::backend::AI_BACKEND_ENV, backend.env_value());
223 }
224
225 if let Some(model) = &self.model {
226 std::env::set_var(crate::claude::backend::MODEL_ENV, model);
227 }
228
229 if let Some(beta_header) = &self.beta_header {
230 std::env::set_var(crate::claude::backend::BETA_HEADER_ENV, beta_header);
231 }
232
233 if self.claude_cli_allow_tools {
237 std::env::set_var("OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS", "true");
238 crate::utils::settings::note_cli_flag_export("OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS");
239 }
240
241 if self.claude_cli_allow_mcp {
242 std::env::set_var("OMNI_DEV_CLAUDE_CLI_ALLOW_MCP", "true");
243 crate::utils::settings::note_cli_flag_export("OMNI_DEV_CLAUDE_CLI_ALLOW_MCP");
244 }
245
246 if let Some(budget) = self.claude_cli_max_budget_usd {
247 std::env::set_var("OMNI_DEV_CLAUDE_CLI_MAX_BUDGET_USD", format!("{budget}"));
248 }
249
250 if let Some(path) = &self.models_yaml {
251 std::env::set_var("OMNI_DEV_MODELS_YAML", path);
252 }
253
254 if let Some(profile) = &self.profile {
259 std::env::set_var(crate::utils::settings::PROFILE_ENV_VAR, profile);
260 }
261
262 if let Some(instance) = &self.instance {
267 std::env::set_var(
268 crate::atlassian::auth::ATLASSIAN_INSTANCE_OVERRIDE_ENV,
269 instance,
270 );
271 }
272 }
273
274 fn validate_active_profile<E, F>(env: &E, load_settings: F) -> Result<()>
280 where
281 E: crate::utils::env::EnvSource,
282 F: FnOnce() -> crate::utils::settings::Settings,
283 {
284 match crate::utils::settings::active_profile_from(env) {
285 Some(name) => load_settings().validate_profile(&name),
286 None => Ok(()),
287 }
288 }
289
290 fn load_settings_or_default() -> crate::utils::settings::Settings {
296 crate::utils::settings::Settings::load().unwrap_or_default()
297 }
298
299 pub async fn execute(self) -> Result<()> {
301 self.propagate_global_flags();
302
303 Self::validate_active_profile(
308 &crate::utils::env::SystemEnv,
309 Self::load_settings_or_default,
310 )?;
311
312 let Self { repo, command, .. } = self;
315 let repo = repo.as_deref();
316
317 match command {
318 Commands::Ai(ai_cmd) => ai_cmd.execute().await,
319 Commands::Git(git_cmd) => git_cmd.execute(repo).await,
320 Commands::Commands(commands_cmd) => commands_cmd.execute(),
321 Commands::Atlassian(cmd) => cmd.execute().await,
322 Commands::Browser(cmd) => cmd.execute().await,
323 #[cfg(unix)]
324 Commands::Daemon(cmd) => cmd.execute().await,
325 Commands::Datadog(cmd) => cmd.execute().await,
326 #[cfg(unix)]
327 Commands::Snowflake(cmd) => cmd.execute().await,
328 #[cfg(unix)]
329 Commands::Worktrees(cmd) => cmd.execute().await,
330 #[cfg(unix)]
331 Commands::Sessions(cmd) => cmd.execute().await,
332 Commands::Coverage(cmd) => cmd.execute(repo).await,
333 Commands::Transcript(cmd) => cmd.execute().await,
334 Commands::Log(log_cmd) => log_cmd.execute(),
335 Commands::Config(config_cmd) => config_cmd.execute(),
336 Commands::Resources(resources_cmd) => resources_cmd.execute(),
337 Commands::Completions(completions_cmd) => completions_cmd.execute(),
338 Commands::HelpAll(help_cmd) => help_cmd.execute(),
339 }
340 }
341}
342
343#[cfg(all(target_os = "macos", feature = "menu-bar"))]
344impl Cli {
345 pub fn menu_bar_run_config(&self) -> Option<Result<crate::daemon::DaemonRunConfig>> {
350 match &self.command {
351 Commands::Daemon(daemon::DaemonCommand {
352 command: daemon::DaemonSubcommands::Run(run),
353 }) if !run.no_menu => Some(run.clone().into_run_config()),
354 _ => None,
355 }
356 }
357}
358
359#[cfg(test)]
360#[allow(clippy::unwrap_used, clippy::expect_used)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn parses_ai_backend_claude_cli() {
366 let cli =
367 Cli::try_parse_from(["omni-dev", "--ai-backend", "claude-cli", "help-all"]).unwrap();
368 assert!(matches!(cli.ai_backend, Some(AiBackend::ClaudeCli)));
369 assert!(!cli.claude_cli_allow_tools);
370 }
371
372 #[test]
373 fn parses_ai_backend_default() {
374 let cli = Cli::try_parse_from(["omni-dev", "--ai-backend", "default", "help-all"]).unwrap();
375 assert!(matches!(cli.ai_backend, Some(AiBackend::Default)));
376 }
377
378 #[test]
379 fn parses_ai_backend_openai_ollama_bedrock() {
380 for (value, expected) in [
381 ("openai", AiBackend::OpenAi),
382 ("ollama", AiBackend::Ollama),
383 ("bedrock", AiBackend::Bedrock),
384 ] {
385 let cli = Cli::try_parse_from(["omni-dev", "--ai-backend", value, "help-all"]).unwrap();
386 assert_eq!(cli.ai_backend, Some(expected), "value {value}");
387 }
388 }
389
390 #[test]
391 fn parses_model_before_and_after_subcommand() {
392 let before = Cli::try_parse_from([
396 "omni-dev",
397 "--model",
398 "claude-opus-4-6",
399 "git",
400 "commit",
401 "message",
402 "twiddle",
403 ])
404 .unwrap();
405 assert_eq!(before.model.as_deref(), Some("claude-opus-4-6"));
406
407 let after = Cli::try_parse_from([
410 "omni-dev",
411 "git",
412 "commit",
413 "message",
414 "twiddle",
415 "--model",
416 "claude-opus-4-6",
417 ])
418 .unwrap();
419 assert_eq!(after.model.as_deref(), Some("claude-opus-4-6"));
420 }
421
422 #[test]
423 fn parses_beta_header_before_and_after_subcommand() {
424 let before = Cli::try_parse_from([
425 "omni-dev",
426 "--beta-header",
427 "anthropic-beta:output-128k-2025-02-19",
428 "git",
429 "commit",
430 "message",
431 "check",
432 ])
433 .unwrap();
434 assert_eq!(
435 before.beta_header.as_deref(),
436 Some("anthropic-beta:output-128k-2025-02-19")
437 );
438
439 let after = Cli::try_parse_from([
440 "omni-dev",
441 "git",
442 "commit",
443 "message",
444 "check",
445 "--beta-header",
446 "anthropic-beta:output-128k-2025-02-19",
447 ])
448 .unwrap();
449 assert_eq!(
450 after.beta_header.as_deref(),
451 Some("anthropic-beta:output-128k-2025-02-19")
452 );
453 }
454
455 #[test]
456 fn parses_ai_backend_absent() {
457 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
458 assert!(cli.ai_backend.is_none());
459 assert!(!cli.claude_cli_allow_tools);
460 assert!(!cli.claude_cli_allow_mcp);
461 }
462
463 #[test]
464 fn parses_claude_cli_allow_tools_flag() {
465 let cli =
466 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-tools", "help-all"]).unwrap();
467 assert!(cli.claude_cli_allow_tools);
468 }
469
470 #[test]
471 fn parses_claude_cli_allow_mcp_flag() {
472 let cli = Cli::try_parse_from(["omni-dev", "--claude-cli-allow-mcp", "help-all"]).unwrap();
473 assert!(cli.claude_cli_allow_mcp);
474 assert!(!cli.claude_cli_allow_tools);
475 }
476
477 #[test]
478 fn allow_mcp_and_allow_tools_are_independent() {
479 let only_mcp =
480 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-mcp", "help-all"]).unwrap();
481 assert!(only_mcp.claude_cli_allow_mcp);
482 assert!(!only_mcp.claude_cli_allow_tools);
483
484 let only_tools =
485 Cli::try_parse_from(["omni-dev", "--claude-cli-allow-tools", "help-all"]).unwrap();
486 assert!(only_tools.claude_cli_allow_tools);
487 assert!(!only_tools.claude_cli_allow_mcp);
488
489 let both = Cli::try_parse_from([
490 "omni-dev",
491 "--claude-cli-allow-tools",
492 "--claude-cli-allow-mcp",
493 "help-all",
494 ])
495 .unwrap();
496 assert!(both.claude_cli_allow_tools);
497 assert!(both.claude_cli_allow_mcp);
498 }
499
500 #[test]
501 fn global_flags_accepted_after_subcommand() {
502 let cli = Cli::try_parse_from([
504 "omni-dev",
505 "help-all",
506 "--ai-backend",
507 "claude-cli",
508 "--claude-cli-allow-tools",
509 ])
510 .unwrap();
511 assert!(matches!(cli.ai_backend, Some(AiBackend::ClaudeCli)));
512 assert!(cli.claude_cli_allow_tools);
513 }
514
515 #[test]
516 fn parses_max_budget_usd_flag() {
517 let cli = Cli::try_parse_from([
518 "omni-dev",
519 "--claude-cli-max-budget-usd",
520 "0.50",
521 "help-all",
522 ])
523 .unwrap();
524 assert_eq!(cli.claude_cli_max_budget_usd, Some(0.50));
525 }
526
527 #[test]
528 fn max_budget_usd_absent_is_none() {
529 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
530 assert!(cli.claude_cli_max_budget_usd.is_none());
531 }
532
533 #[test]
534 fn max_budget_usd_rejects_non_numeric() {
535 let result = Cli::try_parse_from([
536 "omni-dev",
537 "--claude-cli-max-budget-usd",
538 "cheap",
539 "help-all",
540 ]);
541 let Err(err) = result else {
542 panic!("expected parse error for non-numeric budget");
543 };
544 assert!(err.to_string().contains("invalid"));
545 }
546
547 const BACKEND_VAR: &str = "OMNI_DEV_AI_BACKEND";
554 const MODEL_VAR: &str = "OMNI_DEV_MODEL";
555 const BETA_HEADER_VAR: &str = "OMNI_DEV_BETA_HEADER";
556 const ALLOW_TOOLS_VAR: &str = "OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS";
557 const ALLOW_MCP_VAR: &str = "OMNI_DEV_CLAUDE_CLI_ALLOW_MCP";
558 const MAX_BUDGET_VAR: &str = "OMNI_DEV_CLAUDE_CLI_MAX_BUDGET_USD";
559 const MODELS_YAML_VAR: &str = "OMNI_DEV_MODELS_YAML";
560 const PROFILE_VAR: &str = "OMNI_DEV_PROFILE";
561 const INSTANCE_VAR: &str = "OMNI_DEV_ATLASSIAN_INSTANCE";
562
563 struct GlobalFlagsEnvGuard {
566 _lock: std::sync::MutexGuard<'static, ()>,
567 saved: [(&'static str, Option<String>); 9],
568 }
569
570 impl GlobalFlagsEnvGuard {
571 fn new() -> Self {
572 let lock = crate::claude::ai::claude_cli::CLI_ENV_LOCK
573 .lock()
574 .unwrap_or_else(std::sync::PoisonError::into_inner);
575 let names = [
576 BACKEND_VAR,
577 MODEL_VAR,
578 BETA_HEADER_VAR,
579 ALLOW_TOOLS_VAR,
580 ALLOW_MCP_VAR,
581 MAX_BUDGET_VAR,
582 MODELS_YAML_VAR,
583 PROFILE_VAR,
584 INSTANCE_VAR,
585 ];
586 let saved = names.map(|n| (n, std::env::var(n).ok()));
587 for (n, _) in &saved {
588 std::env::remove_var(n);
589 }
590 Self { _lock: lock, saved }
591 }
592 }
593
594 impl Drop for GlobalFlagsEnvGuard {
595 fn drop(&mut self) {
596 for (n, value) in &self.saved {
597 match value {
598 Some(v) => std::env::set_var(n, v),
599 None => std::env::remove_var(n),
600 }
601 }
602 }
603 }
604
605 fn cli_with_defaults() -> Cli {
606 Cli::try_parse_from(["omni-dev", "help-all"]).unwrap()
607 }
608
609 #[test]
610 fn propagate_global_flags_defaults_set_nothing() {
611 let _g = GlobalFlagsEnvGuard::new();
612 cli_with_defaults().propagate_global_flags();
613 assert!(std::env::var(BACKEND_VAR).is_err());
614 assert!(std::env::var(MODEL_VAR).is_err());
615 assert!(std::env::var(BETA_HEADER_VAR).is_err());
616 assert!(std::env::var(ALLOW_TOOLS_VAR).is_err());
617 assert!(std::env::var(ALLOW_MCP_VAR).is_err());
618 assert!(std::env::var(MAX_BUDGET_VAR).is_err());
619 assert!(std::env::var(MODELS_YAML_VAR).is_err());
620 assert!(std::env::var(PROFILE_VAR).is_err());
621 assert!(std::env::var(INSTANCE_VAR).is_err());
622 }
623
624 #[test]
625 fn propagate_global_flags_sets_instance() {
626 let _g = GlobalFlagsEnvGuard::new();
627 let mut cli = cli_with_defaults();
628 cli.instance = Some("https://org.atlassian.net".to_string());
629 cli.propagate_global_flags();
630 assert_eq!(
631 std::env::var(INSTANCE_VAR).ok().as_deref(),
632 Some("https://org.atlassian.net")
633 );
634 }
635
636 #[test]
637 fn propagate_global_flags_sets_ai_backend_claude_cli() {
638 let _g = GlobalFlagsEnvGuard::new();
639 let mut cli = cli_with_defaults();
640 cli.ai_backend = Some(AiBackend::ClaudeCli);
641 cli.propagate_global_flags();
642 assert_eq!(
643 std::env::var(BACKEND_VAR).ok().as_deref(),
644 Some("claude-cli")
645 );
646 }
647
648 #[test]
649 fn propagate_global_flags_default_backend_overrides_env_var() {
650 let _g = GlobalFlagsEnvGuard::new();
654 std::env::set_var(BACKEND_VAR, "claude-cli");
655 let mut cli = cli_with_defaults();
656 cli.ai_backend = Some(AiBackend::Default);
657 cli.propagate_global_flags();
658 assert_eq!(std::env::var(BACKEND_VAR).ok().as_deref(), Some("default"));
659 }
660
661 #[test]
662 fn propagate_global_flags_sets_openai_ollama_bedrock() {
663 let _g = GlobalFlagsEnvGuard::new();
664 for (backend, expected) in [
665 (AiBackend::OpenAi, "openai"),
666 (AiBackend::Ollama, "ollama"),
667 (AiBackend::Bedrock, "bedrock"),
668 ] {
669 let mut cli = cli_with_defaults();
670 cli.ai_backend = Some(backend);
671 cli.propagate_global_flags();
672 assert_eq!(std::env::var(BACKEND_VAR).ok().as_deref(), Some(expected));
673 }
674 }
675
676 #[test]
677 fn propagate_global_flags_sets_model() {
678 let _g = GlobalFlagsEnvGuard::new();
679 let mut cli = cli_with_defaults();
680 cli.model = Some("claude-opus-4-6".to_string());
681 cli.propagate_global_flags();
682 assert_eq!(
683 std::env::var(MODEL_VAR).ok().as_deref(),
684 Some("claude-opus-4-6")
685 );
686 }
687
688 #[test]
689 fn propagate_global_flags_sets_beta_header() {
690 let _g = GlobalFlagsEnvGuard::new();
691 let mut cli = cli_with_defaults();
692 cli.beta_header = Some("anthropic-beta:output-128k-2025-02-19".to_string());
693 cli.propagate_global_flags();
694 assert_eq!(
695 std::env::var(BETA_HEADER_VAR).ok().as_deref(),
696 Some("anthropic-beta:output-128k-2025-02-19")
697 );
698 }
699
700 #[test]
701 fn propagate_global_flags_sets_allow_tools() {
702 let _g = GlobalFlagsEnvGuard::new();
703 let mut cli = cli_with_defaults();
704 cli.claude_cli_allow_tools = true;
705 cli.propagate_global_flags();
706 assert_eq!(std::env::var(ALLOW_TOOLS_VAR).ok().as_deref(), Some("true"));
707 assert!(crate::utils::settings::exported_by_cli_flag(
710 ALLOW_TOOLS_VAR
711 ));
712 }
713
714 #[test]
715 fn propagate_global_flags_sets_allow_mcp() {
716 let _g = GlobalFlagsEnvGuard::new();
717 let mut cli = cli_with_defaults();
718 cli.claude_cli_allow_mcp = true;
719 cli.propagate_global_flags();
720 assert_eq!(std::env::var(ALLOW_MCP_VAR).ok().as_deref(), Some("true"));
721 assert!(crate::utils::settings::exported_by_cli_flag(ALLOW_MCP_VAR));
722 }
723
724 #[test]
725 fn propagate_global_flags_sets_max_budget_usd() {
726 let _g = GlobalFlagsEnvGuard::new();
727 let mut cli = cli_with_defaults();
728 cli.claude_cli_max_budget_usd = Some(1.5);
729 cli.propagate_global_flags();
730 assert_eq!(std::env::var(MAX_BUDGET_VAR).ok().as_deref(), Some("1.5"));
731 }
732
733 #[test]
734 fn parses_models_yaml_flag() {
735 let cli = Cli::try_parse_from([
736 "omni-dev",
737 "--models-yaml",
738 "/tmp/custom-models.yaml",
739 "help-all",
740 ])
741 .unwrap();
742 assert_eq!(
743 cli.models_yaml.as_deref(),
744 Some(std::path::Path::new("/tmp/custom-models.yaml"))
745 );
746 }
747
748 #[test]
749 fn parses_repo_flag_long_and_short() {
750 let long = Cli::try_parse_from(["omni-dev", "--repo", "/tmp/r", "help-all"]).unwrap();
751 assert_eq!(
752 long.repo.as_deref(),
753 Some(std::path::Path::new("/tmp/r")),
754 "--repo should populate cli.repo"
755 );
756 let short = Cli::try_parse_from(["omni-dev", "-C", "/tmp/r", "help-all"]).unwrap();
757 assert_eq!(
758 short.repo.as_deref(),
759 Some(std::path::Path::new("/tmp/r")),
760 "-C should populate cli.repo"
761 );
762 let absent = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
763 assert!(absent.repo.is_none());
764 }
765
766 #[test]
769 fn repo_flag_is_not_propagated_to_env() {
770 let _g = GlobalFlagsEnvGuard::new();
771 let mut cli = cli_with_defaults();
772 cli.repo = Some(std::path::PathBuf::from("/tmp/some-repo"));
773 cli.propagate_global_flags();
774 assert!(
775 std::env::var("OMNI_DEV_REPO").is_err(),
776 "repo must not be exported to an env var"
777 );
778 }
779
780 #[test]
781 fn propagate_global_flags_sets_models_yaml() {
782 let _g = GlobalFlagsEnvGuard::new();
783 let mut cli = cli_with_defaults();
784 cli.models_yaml = Some(std::path::PathBuf::from("/tmp/custom-models.yaml"));
785 cli.propagate_global_flags();
786 assert_eq!(
787 std::env::var(MODELS_YAML_VAR).ok().as_deref(),
788 Some("/tmp/custom-models.yaml")
789 );
790 }
791
792 #[test]
793 fn parses_profile_flag() {
794 let cli = Cli::try_parse_from(["omni-dev", "--profile", "work", "help-all"]).unwrap();
795 assert_eq!(cli.profile.as_deref(), Some("work"));
796 }
797
798 #[test]
799 fn profile_absent_is_none() {
800 let cli = Cli::try_parse_from(["omni-dev", "help-all"]).unwrap();
801 assert!(cli.profile.is_none());
802 }
803
804 #[test]
805 fn propagate_global_flags_sets_profile() {
806 let _g = GlobalFlagsEnvGuard::new();
807 let mut cli = cli_with_defaults();
808 cli.profile = Some("work".to_string());
809 cli.propagate_global_flags();
810 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("work"));
811 }
812
813 #[test]
814 fn propagate_global_flags_profile_flag_beats_env_var() {
815 let _g = GlobalFlagsEnvGuard::new();
816 std::env::set_var(PROFILE_VAR, "personal");
817 let mut cli = cli_with_defaults();
818 cli.profile = Some("work".to_string());
819 cli.propagate_global_flags();
820 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("work"));
821 }
822
823 #[test]
824 fn propagate_global_flags_absent_profile_leaves_env_var() {
825 let _g = GlobalFlagsEnvGuard::new();
826 std::env::set_var(PROFILE_VAR, "personal");
827 cli_with_defaults().propagate_global_flags();
828 assert_eq!(std::env::var(PROFILE_VAR).ok().as_deref(), Some("personal"));
829 }
830
831 #[test]
835 fn validate_active_profile_ok_and_skips_load_when_no_profile() {
836 use crate::test_support::env::MapEnv;
837 let env = MapEnv::new();
838 let result = Cli::validate_active_profile(&env, || panic!("must not load settings"));
839 assert!(result.is_ok());
840 }
841
842 #[test]
843 fn validate_active_profile_ok_for_known_profile() {
844 use crate::test_support::env::MapEnv;
845 use crate::utils::settings::{Profile, Settings};
846 let env = MapEnv::new().with(PROFILE_VAR, "work");
847 let settings = Settings {
848 profiles: std::iter::once(("work".to_string(), Profile::default())).collect(),
849 ..Default::default()
850 };
851 assert!(Cli::validate_active_profile(&env, || settings).is_ok());
852 }
853
854 #[test]
855 fn validate_active_profile_errors_for_unknown_profile() {
856 use crate::test_support::env::MapEnv;
857 use crate::utils::settings::Settings;
858 let env = MapEnv::new().with(PROFILE_VAR, "wrok");
859 let err = Cli::validate_active_profile(&env, Settings::default)
860 .unwrap_err()
861 .to_string();
862 assert!(err.contains("unknown profile 'wrok'"));
863 }
864
865 #[test]
866 fn load_settings_or_default_never_panics() {
867 let _settings = Cli::load_settings_or_default();
871 }
872
873 #[test]
874 fn propagate_global_flags_independent_flags_compose() {
875 let _g = GlobalFlagsEnvGuard::new();
876 let mut cli = cli_with_defaults();
877 cli.ai_backend = Some(AiBackend::ClaudeCli);
878 cli.claude_cli_allow_tools = true;
879 cli.claude_cli_allow_mcp = true;
880 cli.claude_cli_max_budget_usd = Some(0.25);
881 cli.propagate_global_flags();
882 assert_eq!(
883 std::env::var(BACKEND_VAR).ok().as_deref(),
884 Some("claude-cli")
885 );
886 assert_eq!(std::env::var(ALLOW_TOOLS_VAR).ok().as_deref(), Some("true"));
887 assert_eq!(std::env::var(ALLOW_MCP_VAR).ok().as_deref(), Some("true"));
888 assert_eq!(std::env::var(MAX_BUDGET_VAR).ok().as_deref(), Some("0.25"));
889 }
890}