tenseleyflow/claudex / ba8409a

Browse files

tests: fake_claude interactive echo mode for pty tests

Authored by espadonne
SHA
ba8409abe5d12b475c0affa6564e0d99782a5c69
Parents
c9dafb8
Tree
7341589

1 changed file

StatusFile+-
M src-tauri/examples/fake_claude.rs 70 11
src-tauri/examples/fake_claude.rsmodified
@@ -1,9 +1,10 @@
11
 //! 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.
44
 //!
5
-//! Controlled via environment variables:
5
+//! # Modes
66
 //!
7
+//! **Fixture pump** (default): controlled by these env vars:
78
 //! - `FAKE_CLAUDE_STDOUT_FIXTURE` (required): absolute path to a
89
 //!   `.jsonl` file whose lines are pumped to stdout one-at-a-time.
910
 //! - `FAKE_CLAUDE_EXIT_CODE` (default `0`): integer process exit
@@ -12,21 +13,34 @@
1213
 //!   sleep between lines. Used by the cancellation test so the
1314
 //!   kill window is big enough to hit.
1415
 //! - `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.
1724
 //!
1825
 //! 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.
2128
 
2229
 use std::env;
2330
 use std::fs::File;
24
-use std::io::{self, BufRead, BufReader, Write};
31
+use std::io::{self, BufRead, BufReader, Read, Write};
2532
 use std::process::ExitCode;
2633
 use std::thread;
2734
 use std::time::Duration;
2835
 
2936
 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 {
3044
     let fixture = match env::var("FAKE_CLAUDE_STDOUT_FIXTURE") {
3145
         Ok(path) => path,
3246
         Err(_) => {
@@ -62,9 +76,6 @@ fn main() -> ExitCode {
6276
             Err(_) => continue,
6377
         };
6478
         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.
6879
             break;
6980
         }
7081
         if stdout.flush().is_err() {
@@ -81,3 +92,51 @@ fn main() -> ExitCode {
8192
 
8293
     ExitCode::from(exit_code)
8394
 }
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
+}