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:
Yeachan-Heo
2026-04-02 07:37:25 +00:00
parent fd0a299e19
commit b3b14cff79
2 changed files with 163 additions and 7 deletions

View 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()
))
}