mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 08:04:50 +08:00
Prevent resumed slash commands from dropping release-critical arguments
The release harness advertised resumed slash commands like /export <file> and /clear --confirm, but argv parsing split every slash-prefixed token into a new command. That made the claw binary reject legitimate resumed command sequences and quietly miss the caller-provided export target. This change teaches --resume parsing to keep command arguments attached, including absolute export paths, and locks the behavior with both parser regressions and a binary-level smoke test that exercises the real claw resume path. Constraint: Keep the scope to a high-confidence release-path fix that fits a ~1 hour hardening pass Rejected: Broad REPL or network end-to-end coverage expansion | too slow and too wide for the release-confidence target Confidence: high Scope-risk: narrow Reversibility: clean Directive: If new resume-supported commands accept slash-prefixed literals, extend the resume parser heuristics and add binary coverage for them Tested: cargo test --workspace; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test -p rusty-claude-cli parses_resume_flag_with_absolute_export_path -- --exact Not-tested: cargo clippy --workspace --all-targets -- -D warnings currently fails on pre-existing runtime/conversation/session lints outside this change
This commit is contained in:
@@ -425,19 +425,65 @@ fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
||||
.first()
|
||||
.ok_or_else(|| "missing session path for --resume".to_string())
|
||||
.map(PathBuf::from)?;
|
||||
let commands = args[1..].to_vec();
|
||||
if commands
|
||||
.iter()
|
||||
.any(|command| !command.trim_start().starts_with('/'))
|
||||
{
|
||||
return Err("--resume trailing arguments must be slash commands".to_string());
|
||||
let mut commands = Vec::new();
|
||||
let mut current_command = String::new();
|
||||
|
||||
for token in &args[1..] {
|
||||
if token.trim_start().starts_with('/') {
|
||||
if resume_command_can_absorb_token(¤t_command, token) {
|
||||
current_command.push(' ');
|
||||
current_command.push_str(token);
|
||||
continue;
|
||||
}
|
||||
if !current_command.is_empty() {
|
||||
commands.push(current_command);
|
||||
}
|
||||
current_command = token.clone();
|
||||
continue;
|
||||
}
|
||||
|
||||
if current_command.is_empty() {
|
||||
return Err("--resume trailing arguments must be slash commands".to_string());
|
||||
}
|
||||
|
||||
current_command.push(' ');
|
||||
current_command.push_str(token);
|
||||
}
|
||||
|
||||
if !current_command.is_empty() {
|
||||
commands.push(current_command);
|
||||
}
|
||||
|
||||
Ok(CliAction::ResumeSession {
|
||||
session_path,
|
||||
commands,
|
||||
})
|
||||
}
|
||||
|
||||
fn resume_command_can_absorb_token(current_command: &str, token: &str) -> bool {
|
||||
matches!(
|
||||
SlashCommand::parse(current_command),
|
||||
Some(SlashCommand::Export { path: None })
|
||||
) && !looks_like_slash_command_token(token)
|
||||
}
|
||||
|
||||
fn looks_like_slash_command_token(token: &str) -> bool {
|
||||
let trimmed = token.trim_start();
|
||||
let Some(name) = trimmed.strip_prefix('/').and_then(|value| {
|
||||
value
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
slash_command_specs()
|
||||
.iter()
|
||||
.any(|spec| spec.name == name || spec.aliases.contains(&name))
|
||||
}
|
||||
|
||||
fn dump_manifests() {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
|
||||
@@ -3286,7 +3332,6 @@ impl AnthropicRuntimeClient {
|
||||
progress_reporter,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
||||
@@ -4573,6 +4618,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resume_flag_with_slash_command_arguments() {
|
||||
let args = vec![
|
||||
"--resume".to_string(),
|
||||
"session.jsonl".to_string(),
|
||||
"/export".to_string(),
|
||||
"notes.txt".to_string(),
|
||||
"/clear".to_string(),
|
||||
"--confirm".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
parse_args(&args).expect("args should parse"),
|
||||
CliAction::ResumeSession {
|
||||
session_path: PathBuf::from("session.jsonl"),
|
||||
commands: vec![
|
||||
"/export notes.txt".to_string(),
|
||||
"/clear --confirm".to_string()
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resume_flag_with_absolute_export_path() {
|
||||
let args = vec![
|
||||
"--resume".to_string(),
|
||||
"session.jsonl".to_string(),
|
||||
"/export".to_string(),
|
||||
"/tmp/notes.txt".to_string(),
|
||||
"/status".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
parse_args(&args).expect("args should parse"),
|
||||
CliAction::ResumeSession {
|
||||
session_path: PathBuf::from("session.jsonl"),
|
||||
commands: vec!["/export /tmp/notes.txt".to_string(), "/status".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_tool_specs_respect_allowlist() {
|
||||
let allowed = ["read_file", "grep_search"]
|
||||
|
||||
Reference in New Issue
Block a user