use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process;
use std::thread;
use std::time::Duration;
const VERSION: &str = "2.0.0 (Mock Claude Code)";
const HELP_TEXT: &str = r#"mock_cli - Mock Claude CLI for integration testing
USAGE:
mock_cli [OPTIONS]
OPTIONS:
--fixture=<PATH> Path to NDJSON fixture file (required)
--delay=<MS> Delay between messages in milliseconds (default: 20)
--version Print version and exit
--help Print this help text and exit
EXAMPLES:
mock_cli --fixture=tests/fixtures/simple_query.ndjson
mock_cli --fixture=simple_query.ndjson --delay=50
"#;
struct CliArgs {
fixture_path: Option<PathBuf>,
delay_ms: u64,
}
impl CliArgs {
fn parse() -> Result<Self, String> {
let args: Vec<String> = env::args().collect();
let mut fixture_path = None;
let mut delay_ms = 20;
for arg in args.iter().skip(1) {
if arg == "--version" {
println!("{}", VERSION);
process::exit(0);
} else if arg == "--help" {
println!("{}", HELP_TEXT);
process::exit(0);
} else if let Some(path) = arg.strip_prefix("--fixture=") {
fixture_path = Some(PathBuf::from(path));
} else if let Some(delay_str) = arg.strip_prefix("--delay=") {
delay_ms = delay_str
.parse()
.map_err(|_| format!("Invalid delay value: {}", delay_str))?;
} else if arg.starts_with("--output-format=") || arg.starts_with("--model=") {
} else {
return Err(format!("Unknown argument: {}", arg));
}
}
Ok(CliArgs {
fixture_path,
delay_ms,
})
}
}
fn load_fixture(path: &PathBuf) -> io::Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result?;
if line.trim().is_empty() {
continue;
}
if let Err(err) = serde_json::from_str::<serde_json::Value>(&line) {
eprintln!("ERROR: Invalid JSON at line {}: {}", line_num + 1, err);
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid JSON at line {}", line_num + 1),
));
}
lines.push(line);
}
Ok(lines)
}
fn replay_fixture(lines: Vec<String>, delay_ms: u64) -> io::Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
for line in lines {
writeln!(handle, "{}", line)?;
handle.flush()?;
if delay_ms > 0 {
thread::sleep(Duration::from_millis(delay_ms));
}
}
Ok(())
}
fn main() {
let args = match CliArgs::parse() {
Ok(args) => args,
Err(err) => {
eprintln!("ERROR: {}", err);
eprintln!("\nRun 'mock_cli --help' for usage information.");
process::exit(1);
}
};
let fixture_path = match args.fixture_path {
Some(path) => path,
None => {
eprintln!("ERROR: Missing required argument --fixture=<PATH>");
eprintln!("\nRun 'mock_cli --help' for usage information.");
process::exit(1);
}
};
let lines = match load_fixture(&fixture_path) {
Ok(lines) => lines,
Err(err) => {
eprintln!("ERROR: Failed to load fixture: {}", err);
process::exit(1);
}
};
if let Err(err) = replay_fixture(lines, args.delay_ms) {
if err.kind() != io::ErrorKind::BrokenPipe {
eprintln!("ERROR: Failed to replay fixture: {}", err);
process::exit(1);
}
}
process::exit(0);
}