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