mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
Recover the MCP lane on top of current main
This resolves the stale-branch merge against origin/main, keeps the MCP runtime wiring, and preserves prompt-approved CLI tool execution after the mock parity harness additions landed upstream. Constraint: Branch had to absorb origin/main changes through a contentful merge before more MCP work Constraint: Prompt-approved runtime tool execution must continue working with new CLI/mock parity coverage Rejected: Keep permission enforcer attached inside CliToolExecutor for conversation turns | caused prompt-approved bash parity flow to fail as a tool error Rejected: Defer the merge and continue on stale history | would leave the lane red against current main Confidence: high Scope-risk: moderate Reversibility: clean Directive: Runtime permission policy and executor-side permission enforcement are separate layers; do not reapply executor enforcement to conversation turns without revalidating mock parity harness approval flows Tested: cargo test -p rusty-claude-cli --test mock_parity_harness -- --nocapture; cargo test -p rusty-claude-cli -- --nocapture; cargo test --workspace -- --nocapture Not-tested: Additional live remote/provider scenarios beyond the existing workspace suite
This commit is contained in:
@@ -30,9 +30,9 @@ use api::{
|
||||
};
|
||||
|
||||
use commands::{
|
||||
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||
validate_slash_command_input, SlashCommand,
|
||||
handle_agents_slash_command, handle_mcp_slash_command, handle_plugins_slash_command,
|
||||
handle_skills_slash_command, render_slash_command_help, resume_supported_slash_commands,
|
||||
slash_command_specs, validate_slash_command_input, SlashCommand,
|
||||
};
|
||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||
use init::initialize_repo;
|
||||
@@ -40,12 +40,13 @@ use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||
use runtime::{
|
||||
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
|
||||
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient,
|
||||
ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
|
||||
ConversationMessage, ConversationRuntime, McpServerManager, McpTool, MessageRole,
|
||||
OAuthAuthorizationRequest, OAuthConfig, OAuthTokenExchangeRequest, PermissionMode,
|
||||
PermissionPolicy, ProjectContext, PromptCacheEvent, RuntimeError, Session, TokenUsage,
|
||||
ToolError, ToolExecutor, UsageTracker,
|
||||
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials,
|
||||
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
|
||||
ContentBlock, ConversationMessage, ConversationRuntime, McpServerManager, McpTool,
|
||||
MessageRole, ModelPricing, OAuthAuthorizationRequest, OAuthConfig,
|
||||
OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext,
|
||||
PromptCacheEvent, ResolvedPermissionMode, RuntimeError, Session, TokenUsage, ToolError,
|
||||
ToolExecutor, UsageTracker, format_usd, pricing_for_model,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -109,6 +110,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
CliAction::DumpManifests => dump_manifests(),
|
||||
CliAction::BootstrapPlan => print_bootstrap_plan(),
|
||||
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
|
||||
CliAction::Mcp { args } => LiveCli::print_mcp(args.as_deref())?,
|
||||
CliAction::Skills { args } => LiveCli::print_skills(args.as_deref())?,
|
||||
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
||||
CliAction::Version => print_version(),
|
||||
@@ -149,6 +151,9 @@ enum CliAction {
|
||||
Agents {
|
||||
args: Option<String>,
|
||||
},
|
||||
Mcp {
|
||||
args: Option<String>,
|
||||
},
|
||||
Skills {
|
||||
args: Option<String>,
|
||||
},
|
||||
@@ -344,6 +349,9 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
"agents" => Ok(CliAction::Agents {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
}),
|
||||
"mcp" => Ok(CliAction::Mcp {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
}),
|
||||
"skills" => Ok(CliAction::Skills {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
}),
|
||||
@@ -402,6 +410,7 @@ fn bare_slash_command_guidance(command_name: &str) -> Option<String> {
|
||||
"dump-manifests"
|
||||
| "bootstrap-plan"
|
||||
| "agents"
|
||||
| "mcp"
|
||||
| "skills"
|
||||
| "system-prompt"
|
||||
| "login"
|
||||
@@ -437,6 +446,14 @@ fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
match SlashCommand::parse(&raw) {
|
||||
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
|
||||
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }),
|
||||
Ok(Some(SlashCommand::Mcp { action, target })) => Ok(CliAction::Mcp {
|
||||
args: match (action, target) {
|
||||
(None, None) => None,
|
||||
(Some(action), None) => Some(action),
|
||||
(Some(action), Some(target)) => Some(format!("{action} {target}")),
|
||||
(None, Some(target)) => Some(target),
|
||||
},
|
||||
}),
|
||||
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills { args }),
|
||||
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
|
||||
Ok(Some(command)) => Err({
|
||||
@@ -610,12 +627,32 @@ fn permission_mode_from_label(mode: &str) -> PermissionMode {
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_mode_from_resolved(mode: ResolvedPermissionMode) -> PermissionMode {
|
||||
match mode {
|
||||
ResolvedPermissionMode::ReadOnly => PermissionMode::ReadOnly,
|
||||
ResolvedPermissionMode::WorkspaceWrite => PermissionMode::WorkspaceWrite,
|
||||
ResolvedPermissionMode::DangerFullAccess => PermissionMode::DangerFullAccess,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_permission_mode() -> PermissionMode {
|
||||
env::var("RUSTY_CLAUDE_PERMISSION_MODE")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(normalize_permission_mode)
|
||||
.map_or(PermissionMode::DangerFullAccess, permission_mode_from_label)
|
||||
.map(permission_mode_from_label)
|
||||
.or_else(config_permission_mode_for_current_dir)
|
||||
.unwrap_or(PermissionMode::DangerFullAccess)
|
||||
}
|
||||
|
||||
fn config_permission_mode_for_current_dir() -> Option<PermissionMode> {
|
||||
let cwd = env::current_dir().ok()?;
|
||||
let loader = ConfigLoader::default_for(&cwd);
|
||||
loader
|
||||
.load()
|
||||
.ok()?
|
||||
.permission_mode()
|
||||
.map(permission_mode_from_resolved)
|
||||
}
|
||||
|
||||
fn filter_tool_specs(
|
||||
@@ -1309,12 +1346,17 @@ fn run_resume_command(
|
||||
),
|
||||
});
|
||||
}
|
||||
let backup_path = write_session_clear_backup(session, session_path)?;
|
||||
let previous_session_id = session.session_id.clone();
|
||||
let cleared = Session::new();
|
||||
let new_session_id = cleared.session_id.clone();
|
||||
cleared.save_to_path(session_path)?;
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: cleared,
|
||||
message: Some(format!(
|
||||
"Cleared resumed session file {}.",
|
||||
"Session cleared\n Mode resumed session reset\n Previous session {previous_session_id}\n Backup {}\n Resume previous claw --resume {}\n New session {new_session_id}\n Session file {}",
|
||||
backup_path.display(),
|
||||
backup_path.display(),
|
||||
session_path.display()
|
||||
)),
|
||||
})
|
||||
@@ -1361,6 +1403,19 @@ fn run_resume_command(
|
||||
session: session.clone(),
|
||||
message: Some(render_config_report(section.as_deref())?),
|
||||
}),
|
||||
SlashCommand::Mcp { action, target } => {
|
||||
let cwd = env::current_dir()?;
|
||||
let args = match (action.as_deref(), target.as_deref()) {
|
||||
(None, None) => None,
|
||||
(Some(action), None) => Some(action.to_string()),
|
||||
(Some(action), Some(target)) => Some(format!("{action} {target}")),
|
||||
(None, Some(target)) => Some(target.to_string()),
|
||||
};
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
|
||||
})
|
||||
}
|
||||
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(render_memory_report()?),
|
||||
@@ -1417,7 +1472,47 @@ fn run_resume_command(
|
||||
| SlashCommand::Model { .. }
|
||||
| SlashCommand::Permissions { .. }
|
||||
| SlashCommand::Session { .. }
|
||||
| SlashCommand::Plugins { .. } => Err("unsupported resumed slash command".into()),
|
||||
| SlashCommand::Plugins { .. }
|
||||
| SlashCommand::Doctor
|
||||
| SlashCommand::Login
|
||||
| SlashCommand::Logout
|
||||
| SlashCommand::Vim
|
||||
| SlashCommand::Upgrade
|
||||
| SlashCommand::Stats
|
||||
| SlashCommand::Share
|
||||
| SlashCommand::Feedback
|
||||
| SlashCommand::Files
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Exit
|
||||
| SlashCommand::Summary
|
||||
| SlashCommand::Desktop
|
||||
| SlashCommand::Brief
|
||||
| SlashCommand::Advisor
|
||||
| SlashCommand::Stickers
|
||||
| SlashCommand::Insights
|
||||
| SlashCommand::Thinkback
|
||||
| SlashCommand::ReleaseNotes
|
||||
| SlashCommand::SecurityReview
|
||||
| SlashCommand::Keybindings
|
||||
| SlashCommand::PrivacySettings
|
||||
| SlashCommand::Plan { .. }
|
||||
| SlashCommand::Review { .. }
|
||||
| SlashCommand::Tasks { .. }
|
||||
| SlashCommand::Theme { .. }
|
||||
| SlashCommand::Voice { .. }
|
||||
| SlashCommand::Usage { .. }
|
||||
| SlashCommand::Rename { .. }
|
||||
| SlashCommand::Copy { .. }
|
||||
| SlashCommand::Hooks { .. }
|
||||
| SlashCommand::Context { .. }
|
||||
| SlashCommand::Color { .. }
|
||||
| SlashCommand::Effort { .. }
|
||||
| SlashCommand::Branch { .. }
|
||||
| SlashCommand::Rewind { .. }
|
||||
| SlashCommand::Ide { .. }
|
||||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2110,12 +2205,19 @@ impl LiveCli {
|
||||
"output_tokens": summary.usage.output_tokens,
|
||||
"cache_creation_input_tokens": summary.usage.cache_creation_input_tokens,
|
||||
"cache_read_input_tokens": summary.usage.cache_read_input_tokens,
|
||||
}
|
||||
},
|
||||
"estimated_cost": format_usd(
|
||||
summary.usage.estimate_cost_usd_with_pricing(
|
||||
pricing_for_model(&self.model)
|
||||
.unwrap_or_else(runtime::ModelPricing::default_sonnet_tier)
|
||||
).total_cost_usd()
|
||||
)
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn handle_repl_command(
|
||||
&mut self,
|
||||
command: SlashCommand,
|
||||
@@ -2150,7 +2252,7 @@ impl LiveCli {
|
||||
false
|
||||
}
|
||||
SlashCommand::Teleport { target } => {
|
||||
self.run_teleport(target.as_deref())?;
|
||||
Self::run_teleport(target.as_deref())?;
|
||||
false
|
||||
}
|
||||
SlashCommand::DebugToolCall => {
|
||||
@@ -2177,6 +2279,16 @@ impl LiveCli {
|
||||
Self::print_config(section.as_deref())?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Mcp { action, target } => {
|
||||
let args = match (action.as_deref(), target.as_deref()) {
|
||||
(None, None) => None,
|
||||
(Some(action), None) => Some(action.to_string()),
|
||||
(Some(action), Some(target)) => Some(format!("{action} {target}")),
|
||||
(None, Some(target)) => Some(target.to_string()),
|
||||
};
|
||||
Self::print_mcp(args.as_deref())?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Memory => {
|
||||
Self::print_memory()?;
|
||||
false
|
||||
@@ -2211,6 +2323,49 @@ impl LiveCli {
|
||||
Self::print_skills(args.as_deref())?;
|
||||
false
|
||||
}
|
||||
SlashCommand::Doctor
|
||||
| SlashCommand::Login
|
||||
| SlashCommand::Logout
|
||||
| SlashCommand::Vim
|
||||
| SlashCommand::Upgrade
|
||||
| SlashCommand::Stats
|
||||
| SlashCommand::Share
|
||||
| SlashCommand::Feedback
|
||||
| SlashCommand::Files
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Exit
|
||||
| SlashCommand::Summary
|
||||
| SlashCommand::Desktop
|
||||
| SlashCommand::Brief
|
||||
| SlashCommand::Advisor
|
||||
| SlashCommand::Stickers
|
||||
| SlashCommand::Insights
|
||||
| SlashCommand::Thinkback
|
||||
| SlashCommand::ReleaseNotes
|
||||
| SlashCommand::SecurityReview
|
||||
| SlashCommand::Keybindings
|
||||
| SlashCommand::PrivacySettings
|
||||
| SlashCommand::Plan { .. }
|
||||
| SlashCommand::Review { .. }
|
||||
| SlashCommand::Tasks { .. }
|
||||
| SlashCommand::Theme { .. }
|
||||
| SlashCommand::Voice { .. }
|
||||
| SlashCommand::Usage { .. }
|
||||
| SlashCommand::Rename { .. }
|
||||
| SlashCommand::Copy { .. }
|
||||
| SlashCommand::Hooks { .. }
|
||||
| SlashCommand::Context { .. }
|
||||
| SlashCommand::Color { .. }
|
||||
| SlashCommand::Effort { .. }
|
||||
| SlashCommand::Branch { .. }
|
||||
| SlashCommand::Rewind { .. }
|
||||
| SlashCommand::Ide { .. }
|
||||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. } => {
|
||||
eprintln!("Command registered but not yet implemented.");
|
||||
false
|
||||
}
|
||||
SlashCommand::Unknown(name) => {
|
||||
eprintln!("{}", format_unknown_slash_command(&name));
|
||||
false
|
||||
@@ -2358,6 +2513,7 @@ impl LiveCli {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let previous_session = self.session.clone();
|
||||
let session_state = Session::new();
|
||||
self.session = create_managed_session_handle(&session_state.session_id)?;
|
||||
let runtime = build_runtime(
|
||||
@@ -2373,10 +2529,13 @@ impl LiveCli {
|
||||
)?;
|
||||
self.replace_runtime(runtime)?;
|
||||
println!(
|
||||
"Session cleared\n Mode fresh session\n Preserved model {}\n Permission mode {}\n Session {}",
|
||||
"Session cleared\n Mode fresh session\n Previous session {}\n Resume previous /resume {}\n Preserved model {}\n Permission mode {}\n New session {}\n Session file {}",
|
||||
previous_session.id,
|
||||
previous_session.id,
|
||||
self.model,
|
||||
self.permission_mode.as_str(),
|
||||
self.session.id,
|
||||
self.session.path.display(),
|
||||
);
|
||||
Ok(true)
|
||||
}
|
||||
@@ -2442,6 +2601,12 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_mcp(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
println!("{}", handle_mcp_slash_command(args, &cwd)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_skills(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
println!("{}", handle_skills_slash_command(args, &cwd)?);
|
||||
@@ -2655,8 +2820,7 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn run_teleport(&self, target: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn run_teleport(target: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(target) = target.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
println!("Usage: /teleport <symbol-or-path>");
|
||||
return Ok(());
|
||||
@@ -2906,6 +3070,27 @@ fn format_session_modified_age(modified_epoch_millis: u128) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_session_clear_backup(
|
||||
session: &Session,
|
||||
session_path: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
let backup_path = session_clear_backup_path(session_path);
|
||||
session.save_to_path(&backup_path)?;
|
||||
Ok(backup_path)
|
||||
}
|
||||
|
||||
fn session_clear_backup_path(session_path: &Path) -> PathBuf {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.map_or(0, |duration| duration.as_millis());
|
||||
let file_name = session_path
|
||||
.file_name()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("session.jsonl");
|
||||
session_path.with_file_name(format!("{file_name}.before-clear-{timestamp}.bak"))
|
||||
}
|
||||
|
||||
fn render_repl_help() -> String {
|
||||
[
|
||||
"REPL".to_string(),
|
||||
@@ -3674,7 +3859,7 @@ fn build_runtime_plugin_state_with_loader(
|
||||
loader: &ConfigLoader,
|
||||
runtime_config: &runtime::RuntimeConfig,
|
||||
) -> Result<RuntimePluginState, Box<dyn std::error::Error>> {
|
||||
let plugin_manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
||||
let plugin_manager = build_plugin_manager(cwd, loader, runtime_config);
|
||||
let plugin_registry = plugin_manager.plugin_registry()?;
|
||||
let plugin_hook_config =
|
||||
runtime_hook_config_from_plugin_hooks(plugin_registry.aggregated_hooks()?);
|
||||
@@ -4114,6 +4299,8 @@ fn build_runtime_with_plugin_state(
|
||||
mcp_state,
|
||||
} = runtime_plugin_state;
|
||||
plugin_registry.initialize()?;
|
||||
let policy = permission_policy(permission_mode, &feature_config, &tool_registry)
|
||||
.map_err(std::io::Error::other)?;
|
||||
let mut runtime = ConversationRuntime::new_with_features(
|
||||
session,
|
||||
AnthropicRuntimeClient::new(
|
||||
@@ -4131,8 +4318,7 @@ fn build_runtime_with_plugin_state(
|
||||
tool_registry.clone(),
|
||||
mcp_state.clone(),
|
||||
),
|
||||
permission_policy(permission_mode, &feature_config, &tool_registry)
|
||||
.map_err(std::io::Error::other)?,
|
||||
policy,
|
||||
system_prompt,
|
||||
&feature_config,
|
||||
);
|
||||
@@ -4508,6 +4694,9 @@ fn slash_command_completion_candidates_with_sessions(
|
||||
"/config hooks",
|
||||
"/config model",
|
||||
"/config plugins",
|
||||
"/mcp ",
|
||||
"/mcp list",
|
||||
"/mcp show ",
|
||||
"/export ",
|
||||
"/issue ",
|
||||
"/model ",
|
||||
@@ -4533,6 +4722,7 @@ fn slash_command_completion_candidates_with_sessions(
|
||||
"/teleport ",
|
||||
"/ultraplan ",
|
||||
"/agents help",
|
||||
"/mcp help",
|
||||
"/skills help",
|
||||
] {
|
||||
completions.insert(candidate.to_string());
|
||||
@@ -5293,6 +5483,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw dump-manifests")?;
|
||||
writeln!(out, " claw bootstrap-plan")?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw mcp")?;
|
||||
writeln!(out, " claw skills")?;
|
||||
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
||||
writeln!(out, " claw login")?;
|
||||
@@ -5364,6 +5555,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
" claw --resume {LATEST_SESSION_REFERENCE} /status /diff /export notes.txt"
|
||||
)?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw mcp show my-server")?;
|
||||
writeln!(out, " claw /skills")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw init")?;
|
||||
@@ -5516,6 +5708,8 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn defaults_to_repl_when_no_args() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
assert_eq!(
|
||||
parse_args(&[]).expect("args should parse"),
|
||||
CliAction::Repl {
|
||||
@@ -5526,8 +5720,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_permission_mode_uses_project_config_when_env_is_unset() {
|
||||
let _guard = env_lock();
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let config_home = root.join("config-home");
|
||||
std::fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist");
|
||||
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
std::fs::write(
|
||||
cwd.join(".claw").join("settings.json"),
|
||||
r#"{"permissionMode":"acceptEdits"}"#,
|
||||
)
|
||||
.expect("project config should write");
|
||||
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_permission_mode = std::env::var("RUSTY_CLAUDE_PERMISSION_MODE").ok();
|
||||
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
|
||||
let resolved = with_current_dir(&cwd, super::default_permission_mode);
|
||||
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_permission_mode {
|
||||
Some(value) => std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", value),
|
||||
None => std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE"),
|
||||
}
|
||||
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||
|
||||
assert_eq!(resolved, PermissionMode::WorkspaceWrite);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_permission_mode_overrides_project_config_default() {
|
||||
let _guard = env_lock();
|
||||
let root = temp_dir();
|
||||
let cwd = root.join("project");
|
||||
let config_home = root.join("config-home");
|
||||
std::fs::create_dir_all(cwd.join(".claw")).expect("project config dir should exist");
|
||||
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
std::fs::write(
|
||||
cwd.join(".claw").join("settings.json"),
|
||||
r#"{"permissionMode":"acceptEdits"}"#,
|
||||
)
|
||||
.expect("project config should write");
|
||||
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_permission_mode = std::env::var("RUSTY_CLAUDE_PERMISSION_MODE").ok();
|
||||
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||
std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", "read-only");
|
||||
|
||||
let resolved = with_current_dir(&cwd, super::default_permission_mode);
|
||||
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_permission_mode {
|
||||
Some(value) => std::env::set_var("RUSTY_CLAUDE_PERMISSION_MODE", value),
|
||||
None => std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE"),
|
||||
}
|
||||
std::fs::remove_dir_all(root).expect("temp config root should clean up");
|
||||
|
||||
assert_eq!(resolved, PermissionMode::ReadOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_prompt_subcommand() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec![
|
||||
"prompt".to_string(),
|
||||
"hello".to_string(),
|
||||
@@ -5547,6 +5811,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_bare_prompt_and_json_output_flag() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec![
|
||||
"--output-format=json".to_string(),
|
||||
"--model".to_string(),
|
||||
@@ -5568,6 +5834,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolves_model_aliases_in_args() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec![
|
||||
"--model".to_string(),
|
||||
"opus".to_string(),
|
||||
@@ -5621,6 +5889,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_allowed_tools_flags_with_aliases_and_lists() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
let args = vec![
|
||||
"--allowedTools".to_string(),
|
||||
"read,glob".to_string(),
|
||||
@@ -5684,6 +5954,10 @@ mod tests {
|
||||
parse_args(&["agents".to_string()]).expect("agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["mcp".to_string()]).expect("mcp should parse"),
|
||||
CliAction::Mcp { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["skills".to_string()]).expect("skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
@@ -5699,6 +5973,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
assert_eq!(
|
||||
parse_args(&["help".to_string()]).expect("help should parse"),
|
||||
CliAction::Help
|
||||
@@ -5729,6 +6005,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn multi_word_prompt_still_uses_shorthand_prompt_mode() {
|
||||
let _guard = env_lock();
|
||||
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
|
||||
assert_eq!(
|
||||
parse_args(&["help".to_string(), "me".to_string(), "debug".to_string()])
|
||||
.expect("prompt shorthand should still work"),
|
||||
@@ -5743,11 +6021,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_direct_agents_and_skills_slash_commands() {
|
||||
fn parses_direct_agents_mcp_and_skills_slash_commands() {
|
||||
assert_eq!(
|
||||
parse_args(&["/agents".to_string()]).expect("/agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()])
|
||||
.expect("/mcp show demo should parse"),
|
||||
CliAction::Mcp {
|
||||
args: Some("show demo".to_string())
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string()]).expect("/skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
@@ -5795,9 +6080,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_slash_command_with_suggestions() {
|
||||
let report = format_unknown_slash_command_message("stats");
|
||||
assert!(report.contains("unknown slash command: /stats"));
|
||||
assert!(report.contains("Did you mean /status?"));
|
||||
let report = format_unknown_slash_command_message("statsu");
|
||||
assert!(report.contains("unknown slash command: /statsu"));
|
||||
assert!(report.contains("Did you mean"));
|
||||
assert!(report.contains("Use /help"));
|
||||
}
|
||||
|
||||
@@ -5965,6 +6250,7 @@ mod tests {
|
||||
assert!(help.contains("/cost"));
|
||||
assert!(help.contains("/resume <session-path>"));
|
||||
assert!(help.contains("/config [env|hooks|model|plugins]"));
|
||||
assert!(help.contains("/mcp [list|show <server>|help]"));
|
||||
assert!(help.contains("/memory"));
|
||||
assert!(help.contains("/init"));
|
||||
assert!(help.contains("/diff"));
|
||||
@@ -5995,13 +6281,15 @@ mod tests {
|
||||
assert!(completions.contains(&"/session list".to_string()));
|
||||
assert!(completions.contains(&"/session switch session-current".to_string()));
|
||||
assert!(completions.contains(&"/resume session-old".to_string()));
|
||||
assert!(completions.contains(&"/mcp list".to_string()));
|
||||
assert!(completions.contains(&"/ultraplan ".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires ANTHROPIC_API_KEY"]
|
||||
fn startup_banner_mentions_workflow_completions() {
|
||||
let _guard = env_lock();
|
||||
// Inject dummy credentials so LiveCli can construct without real Anthropic key
|
||||
std::env::set_var("ANTHROPIC_API_KEY", "test-dummy-key-for-banner-test");
|
||||
let root = temp_dir();
|
||||
fs::create_dir_all(&root).expect("root dir");
|
||||
|
||||
@@ -6020,6 +6308,7 @@ mod tests {
|
||||
assert!(banner.contains("workflow completions"));
|
||||
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6028,13 +6317,12 @@ mod tests {
|
||||
.into_iter()
|
||||
.map(|spec| spec.name)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![
|
||||
"help", "status", "sandbox", "compact", "clear", "cost", "config", "memory",
|
||||
"init", "diff", "version", "export", "agents", "skills",
|
||||
]
|
||||
);
|
||||
// Now with 135+ slash commands, verify minimum resume support
|
||||
assert!(names.len() >= 39, "expected at least 39 resume-supported commands, got {}", names.len());
|
||||
// Verify key resume commands still exist
|
||||
assert!(names.contains(&"help"));
|
||||
assert!(names.contains(&"status"));
|
||||
assert!(names.contains(&"compact"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6104,6 +6392,7 @@ mod tests {
|
||||
assert!(help.contains("claw sandbox"));
|
||||
assert!(help.contains("claw init"));
|
||||
assert!(help.contains("claw agents"));
|
||||
assert!(help.contains("claw mcp"));
|
||||
assert!(help.contains("claw skills"));
|
||||
assert!(help.contains("claw /skills"));
|
||||
}
|
||||
@@ -7116,6 +7405,9 @@ UU conflicted.rs",
|
||||
#[test]
|
||||
fn build_runtime_runs_plugin_lifecycle_init_and_shutdown() {
|
||||
let config_home = temp_dir();
|
||||
// Inject a dummy API key so runtime construction succeeds without real credentials.
|
||||
// This test only exercises plugin lifecycle (init/shutdown), never calls the API.
|
||||
std::env::set_var("ANTHROPIC_API_KEY", "test-dummy-key-for-plugin-lifecycle");
|
||||
let workspace = temp_dir();
|
||||
let source_root = temp_dir();
|
||||
fs::create_dir_all(&config_home).expect("config home");
|
||||
@@ -7164,6 +7456,7 @@ UU conflicted.rs",
|
||||
let _ = fs::remove_dir_all(config_home);
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
let _ = fs::remove_dir_all(source_root);
|
||||
std::env::remove_var("ANTHROPIC_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user