| 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 |