Rust · 4546 bytes Raw Blame History
1 //! Test-only stand-in for the real `claude` CLI. The chat driver
2 //! and pty driver tests point their `claude_bin` at this binary
3 //! instead of the real one so tests are deterministic and offline.
4 //!
5 //! # Modes
6 //!
7 //! **Fixture pump** (default): controlled by these env vars:
8 //! - `FAKE_CLAUDE_STDOUT_FIXTURE` (required): absolute path to a
9 //! `.jsonl` file whose lines are pumped to stdout one-at-a-time.
10 //! - `FAKE_CLAUDE_EXIT_CODE` (default `0`): integer process exit
11 //! code after pumping finishes.
12 //! - `FAKE_CLAUDE_LINE_DELAY_MS` (default `0`): milliseconds to
13 //! sleep between lines. Used by the cancellation test so the
14 //! kill window is big enough to hit.
15 //! - `FAKE_CLAUDE_STDERR_LINE` (optional): single line written to
16 //! stderr just before exit.
17 //!
18 //! **Interactive echo** (for PTY tests): controlled by
19 //! `FAKE_CLAUDE_INTERACTIVE=1`. Reads from stdin and echoes each
20 //! byte back to stdout with a `> ` prefix per line. Runs until
21 //! stdin EOFs or the process is killed. Used to verify the
22 //! `spawn_pty → write_pty → read → close` round-trip in
23 //! `pty_driver_test`. Ignores the fixture env vars entirely.
24 //!
25 //! Invocation args from the driver (`-p`, `--output-format`, etc.)
26 //! are ignored either way — we only care about the output
27 //! contract, not the input contract.
28
29 use std::env;
30 use std::fs::File;
31 use std::io::{self, BufRead, BufReader, Read, Write};
32 use std::process::ExitCode;
33 use std::thread;
34 use std::time::Duration;
35
36 fn main() -> ExitCode {
37 if env::var("FAKE_CLAUDE_INTERACTIVE").ok().as_deref() == Some("1") {
38 return run_interactive();
39 }
40 run_fixture_pump()
41 }
42
43 fn run_fixture_pump() -> ExitCode {
44 let fixture = match env::var("FAKE_CLAUDE_STDOUT_FIXTURE") {
45 Ok(path) => path,
46 Err(_) => {
47 eprintln!("fake_claude: FAKE_CLAUDE_STDOUT_FIXTURE not set");
48 return ExitCode::from(2);
49 }
50 };
51 let exit_code: u8 = env::var("FAKE_CLAUDE_EXIT_CODE")
52 .ok()
53 .and_then(|s| s.parse().ok())
54 .unwrap_or(0);
55 let delay_ms: u64 = env::var("FAKE_CLAUDE_LINE_DELAY_MS")
56 .ok()
57 .and_then(|s| s.parse().ok())
58 .unwrap_or(0);
59 let stderr_line = env::var("FAKE_CLAUDE_STDERR_LINE").ok();
60
61 let file = match File::open(&fixture) {
62 Ok(f) => f,
63 Err(err) => {
64 eprintln!("fake_claude: cannot open {fixture}: {err}");
65 return ExitCode::from(3);
66 }
67 };
68
69 let reader = BufReader::new(file);
70 let stdout = io::stdout();
71 let mut stdout = stdout.lock();
72
73 for line in reader.lines() {
74 let line = match line {
75 Ok(l) => l,
76 Err(_) => continue,
77 };
78 if writeln!(stdout, "{line}").is_err() {
79 break;
80 }
81 if stdout.flush().is_err() {
82 break;
83 }
84 if delay_ms > 0 {
85 thread::sleep(Duration::from_millis(delay_ms));
86 }
87 }
88
89 if let Some(line) = stderr_line {
90 eprintln!("{line}");
91 }
92
93 ExitCode::from(exit_code)
94 }
95
96 /// Interactive echo: read bytes from stdin, echo them back with a
97 /// `> ` prefix per line. Prints a READY marker on startup so tests
98 /// can block until the subprocess is ready for input without a
99 /// timing race. Exits when stdin EOFs.
100 fn run_interactive() -> ExitCode {
101 let stdout = io::stdout();
102 let mut out = stdout.lock();
103 let _ = writeln!(out, "READY");
104 let _ = out.flush();
105
106 let stdin = io::stdin();
107 let mut buf = [0u8; 256];
108 let mut line = Vec::<u8>::new();
109
110 loop {
111 let n = match stdin.lock().read(&mut buf) {
112 Ok(0) => break, // EOF
113 Ok(n) => n,
114 Err(_) => break,
115 };
116 for &b in &buf[..n] {
117 line.push(b);
118 // Echo on newline; PTY line-discipline usually flushes
119 // on '\r' (CR) because terminal input arrives in cooked
120 // mode. Be tolerant of both.
121 if b == b'\n' || b == b'\r' {
122 let _ = out.write_all(b"> ");
123 let _ = out.write_all(&line);
124 if b == b'\r' {
125 let _ = out.write_all(b"\n");
126 }
127 let _ = out.flush();
128 line.clear();
129 }
130 }
131 }
132
133 // Emit any trailing partial line before exiting.
134 if !line.is_empty() {
135 let _ = out.write_all(b"> ");
136 let _ = out.write_all(&line);
137 let _ = out.write_all(b"\n");
138 let _ = out.flush();
139 }
140
141 ExitCode::from(0)
142 }
143