mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-06 19:28:49 +03:00
Merge remote-tracking branch 'origin/omx-issue-9202-release-harness'
# Conflicts: # rust/crates/rusty-claude-cli/src/main.rs
This commit is contained in:
@@ -571,25 +571,72 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
let (session_path, commands) = match args.first() {
|
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
|
||||||
None => (PathBuf::from(LATEST_SESSION_REFERENCE), Vec::new()),
|
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
|
||||||
Some(first) if first.trim_start().starts_with('/') => {
|
Some(first) if first.trim_start().starts_with('/') => {
|
||||||
(PathBuf::from(LATEST_SESSION_REFERENCE), args.to_vec())
|
(PathBuf::from(LATEST_SESSION_REFERENCE), args)
|
||||||
}
|
}
|
||||||
Some(first) => (PathBuf::from(first), args[1..].to_vec()),
|
Some(first) => (PathBuf::from(first), &args[1..]),
|
||||||
};
|
};
|
||||||
if commands
|
let mut commands = Vec::new();
|
||||||
.iter()
|
let mut current_command = String::new();
|
||||||
.any(|command| !command.trim_start().starts_with('/'))
|
|
||||||
{
|
for token in command_tokens {
|
||||||
|
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());
|
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 {
|
Ok(CliAction::ResumeSession {
|
||||||
session_path,
|
session_path,
|
||||||
commands,
|
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() {
|
fn dump_manifests() {
|
||||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
|
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
|
||||||
@@ -5118,6 +5165,46 @@ mod tests {
|
|||||||
assert!(error.contains("claw --help"));
|
assert!(error.contains("claw --help"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn filtered_tool_specs_respect_allowlist() {
|
fn filtered_tool_specs_respect_allowlist() {
|
||||||
let allowed = ["read_file", "grep_search"]
|
let allowed = ["read_file", "grep_search"]
|
||||||
|
|||||||
71
rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs
Normal file
71
rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use runtime::Session;
|
||||||
|
|
||||||
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resumed_binary_accepts_slash_commands_with_arguments() {
|
||||||
|
let temp_dir = unique_temp_dir("resume-slash-commands");
|
||||||
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
|
|
||||||
|
let session_path = temp_dir.join("session.jsonl");
|
||||||
|
let export_path = temp_dir.join("notes.txt");
|
||||||
|
|
||||||
|
let mut session = Session::new();
|
||||||
|
session
|
||||||
|
.push_user_text("ship the slash command harness")
|
||||||
|
.expect("session write should succeed");
|
||||||
|
session
|
||||||
|
.save_to_path(&session_path)
|
||||||
|
.expect("session should persist");
|
||||||
|
|
||||||
|
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||||
|
.current_dir(&temp_dir)
|
||||||
|
.args([
|
||||||
|
"--resume",
|
||||||
|
session_path.to_str().expect("utf8 path"),
|
||||||
|
"/export",
|
||||||
|
export_path.to_str().expect("utf8 path"),
|
||||||
|
"/clear",
|
||||||
|
"--confirm",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.expect("claw should launch");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||||
|
assert!(stdout.contains("Export"));
|
||||||
|
assert!(stdout.contains("wrote transcript"));
|
||||||
|
assert!(stdout.contains(export_path.to_str().expect("utf8 path")));
|
||||||
|
assert!(stdout.contains("Cleared resumed session file"));
|
||||||
|
|
||||||
|
let export = fs::read_to_string(&export_path).expect("export file should exist");
|
||||||
|
assert!(export.contains("# Conversation Export"));
|
||||||
|
assert!(export.contains("ship the slash command harness"));
|
||||||
|
|
||||||
|
let restored = Session::load_from_path(&session_path).expect("cleared session should load");
|
||||||
|
assert!(restored.messages.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
||||||
|
let millis = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("clock should be after epoch")
|
||||||
|
.as_millis();
|
||||||
|
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"claw-{label}-{}-{millis}-{counter}",
|
||||||
|
std::process::id()
|
||||||
|
))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user