@@ -1,9 +1,10 @@ |
| 1 | 1 | //! Test-only stand-in for the real `claude` CLI. The chat driver |
| 2 | | -//! tests point `TurnRequest.claude_bin` at this binary instead of |
| 3 | | -//! the real one so they're deterministic and offline. |
| 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 | 4 | //! |
| 5 | | -//! Controlled via environment variables: |
| 5 | +//! # Modes |
| 6 | 6 | //! |
| 7 | +//! **Fixture pump** (default): controlled by these env vars: |
| 7 | 8 | //! - `FAKE_CLAUDE_STDOUT_FIXTURE` (required): absolute path to a |
| 8 | 9 | //! `.jsonl` file whose lines are pumped to stdout one-at-a-time. |
| 9 | 10 | //! - `FAKE_CLAUDE_EXIT_CODE` (default `0`): integer process exit |
@@ -12,21 +13,34 @@ |
| 12 | 13 | //! sleep between lines. Used by the cancellation test so the |
| 13 | 14 | //! kill window is big enough to hit. |
| 14 | 15 | //! - `FAKE_CLAUDE_STDERR_LINE` (optional): single line written to |
| 15 | | -//! stderr just before exit. Lets tests exercise the stderr |
| 16 | | -//! forwarding path without a runaway subprocess. |
| 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. |
| 17 | 24 | //! |
| 18 | 25 | //! Invocation args from the driver (`-p`, `--output-format`, etc.) |
| 19 | | -//! are ignored entirely — we only care about the output, not the |
| 20 | | -//! input contract. |
| 26 | +//! are ignored either way — we only care about the output |
| 27 | +//! contract, not the input contract. |
| 21 | 28 | |
| 22 | 29 | use std::env; |
| 23 | 30 | use std::fs::File; |
| 24 | | -use std::io::{self, BufRead, BufReader, Write}; |
| 31 | +use std::io::{self, BufRead, BufReader, Read, Write}; |
| 25 | 32 | use std::process::ExitCode; |
| 26 | 33 | use std::thread; |
| 27 | 34 | use std::time::Duration; |
| 28 | 35 | |
| 29 | 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 { |
| 30 | 44 | let fixture = match env::var("FAKE_CLAUDE_STDOUT_FIXTURE") { |
| 31 | 45 | Ok(path) => path, |
| 32 | 46 | Err(_) => { |
@@ -62,9 +76,6 @@ fn main() -> ExitCode { |
| 62 | 76 | Err(_) => continue, |
| 63 | 77 | }; |
| 64 | 78 | if writeln!(stdout, "{line}").is_err() { |
| 65 | | - // Pipe closed — driver cancelled us. Exit cleanly; the |
| 66 | | - // driver's wait task will observe the exit code we're |
| 67 | | - // about to return. |
| 68 | 79 | break; |
| 69 | 80 | } |
| 70 | 81 | if stdout.flush().is_err() { |
@@ -81,3 +92,51 @@ fn main() -> ExitCode { |
| 81 | 92 | |
| 82 | 93 | ExitCode::from(exit_code) |
| 83 | 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 | +} |