From f10bf8f595cca5200ab42db8fcc1a3d34a6764c8 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Fri, 3 Apr 2026 09:37:46 +0000 Subject: [PATCH] Keep argument parsing independent from plugin discovery parse_args only needs the tool registry when --allowedTools is present, but it was eagerly normalizing an empty list through the plugin-backed registry. On a clean CI home this made no-arg parsing fail if bundled plugin sync surfaced a broken install, and it also left parse-related tests exposed to concurrent environment mutation. Short-circuit empty allowed-tool normalization and serialize the default-permission parse tests that depend on shared process env. Constraint: Rust CI runs unit tests in parallel inside one process, so std::env mutations are shared across tests Rejected: Keep eager registry loading for empty allowed-tools lists | unnecessary work and leaks unrelated plugin failures into basic arg parsing Confidence: high Scope-risk: narrow Reversibility: clean Directive: Any test that mutates HOME, CLAW_CONFIG_HOME, or RUSTY_CLAUDE_PERMISSION_MODE must hold env_lock while code under test reads process env Tested: cargo fmt --all --check; cargo test -p rusty-claude-cli Not-tested: Additional remote workflows beyond rust-ci --- rust/crates/rusty-claude-cli/src/main.rs | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4091626..2fe5bd6 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -587,6 +587,9 @@ fn resolve_model_alias(model: &str) -> &str { } fn normalize_allowed_tools(values: &[String]) -> Result, String> { + if values.is_empty() { + return Ok(None); + } current_tool_registry()?.normalize_allowed_tools(values) } @@ -5318,6 +5321,7 @@ mod tests { } #[test] fn defaults_to_repl_when_no_args() { + let _guard = env_lock(); assert_eq!( parse_args(&[]).expect("args should parse"), CliAction::Repl { @@ -5328,6 +5332,62 @@ mod tests { ); } + #[test] + fn defaults_to_repl_when_no_args_without_loading_plugin_registry() { + let _guard = env_lock(); + let root = temp_dir(); + let cwd = root.join("workspace"); + let config_home = root.join("config-home"); + let bundled_root = cwd.join("broken-bundled"); + let plugin_root = bundled_root.join("broken-hooks"); + + std::fs::create_dir_all(plugin_root.join(".claude-plugin")) + .expect("broken bundled manifest dir should exist"); + std::fs::create_dir_all(&config_home).expect("config home should exist"); + std::fs::write( + plugin_root.join(".claude-plugin").join("plugin.json"), + r#"{ + "name": "broken-hooks", + "version": "1.0.0", + "description": "broken bundled fixture", + "hooks": { + "PreToolUse": ["./hooks/pre.sh"] + } +}"#, + ) + .expect("broken bundled manifest should write"); + std::fs::write( + config_home.join("settings.json"), + serde_json::json!({ + "plugins": { + "bundledRoot": "./broken-bundled" + } + }) + .to_string(), + ) + .expect("config should write"); + + let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok(); + std::env::set_var("CLAW_CONFIG_HOME", &config_home); + + let action = with_current_dir(&cwd, || parse_args(&[]).expect("args should parse")); + + match original_config_home { + Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value), + None => std::env::remove_var("CLAW_CONFIG_HOME"), + } + std::fs::remove_dir_all(root).expect("temp config root should clean up"); + + assert_eq!( + action, + CliAction::Repl { + model: DEFAULT_MODEL.to_string(), + allowed_tools: None, + permission_mode: PermissionMode::DangerFullAccess, + } + ); + } + #[test] fn default_permission_mode_uses_project_config_when_env_is_unset() { let _guard = env_lock(); @@ -5398,6 +5458,7 @@ mod tests { #[test] fn parses_prompt_subcommand() { + let _guard = env_lock(); let args = vec![ "prompt".to_string(), "hello".to_string(), @@ -5417,6 +5478,7 @@ mod tests { #[test] fn parses_bare_prompt_and_json_output_flag() { + let _guard = env_lock(); let args = vec![ "--output-format=json".to_string(), "--model".to_string(), @@ -5438,6 +5500,7 @@ mod tests { #[test] fn resolves_model_aliases_in_args() { + let _guard = env_lock(); let args = vec![ "--model".to_string(), "opus".to_string(), @@ -5491,6 +5554,7 @@ mod tests { #[test] fn parses_allowed_tools_flags_with_aliases_and_lists() { + let _guard = env_lock(); let args = vec![ "--allowedTools".to_string(), "read,glob".to_string(), @@ -5573,6 +5637,7 @@ mod tests { #[test] fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() { + let _guard = env_lock(); assert_eq!( parse_args(&["help".to_string()]).expect("help should parse"), CliAction::Help @@ -5603,6 +5668,7 @@ mod tests { #[test] fn multi_word_prompt_still_uses_shorthand_prompt_mode() { + let _guard = env_lock(); assert_eq!( parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()]) .expect("prompt shorthand should still work"),