From e25568437f4c4a886389d9c67d85d4fc86e8c711 Mon Sep 17 00:00:00 2001 From: "firat.sertgoz" Date: Sat, 18 Apr 2026 16:32:28 +0300 Subject: [PATCH] fix(repl): unlock stdin after auth prompt (#2640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(repl): unlock stdin after auth prompt * test(repl): drive auth-required unlock through input polling loop Address Claude bot review on PR #2640: per `.claude/rules/testing.md` ("Test Through the Caller, Not Just the Helper"), a test that only asserts the `stdin_locked` flag flipped is insufficient regression coverage — the side effect that matters is the input thread's polling loop in `start()` actually resuming. The new test mirrors that polling pattern in a worker thread and asserts it observes the unlock after `send_status(AuthRequired)`, so a future regression that skipped the store would fail the test rather than pass it silently. https://claude.ai/code/session_01VgFn7fywjFgKbFQtwkrrNd --------- Co-authored-by: Claude --- src/channels/repl.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/channels/repl.rs b/src/channels/repl.rs index 79c1f8e9e2..a35941a1be 100644 --- a/src/channels/repl.rs +++ b/src/channels/repl.rs @@ -747,6 +747,9 @@ impl Channel for ReplChannel { eprintln!(" {}{url}{}", fmt::link(), fmt::reset()); } eprintln!(); + // Resume readline so the user can paste a token or continue the + // auth flow via the normal submission parser path. + self.stdin_locked.store(false, Ordering::Relaxed); } StatusUpdate::AuthCompleted { extension_name, @@ -876,4 +879,51 @@ mod tests { "stream should end after the single message" ); } + + /// Regression: per `.claude/rules/testing.md` ("Test Through the Caller, + /// Not Just the Helper"), a unit test that only asserts the + /// `stdin_locked` flag flipped is insufficient — the side effect that + /// matters is the input thread's polling loop in `start()` actually + /// resuming. This test mirrors that polling pattern and asserts it + /// observes the unlock after `send_status(AuthRequired)`. + #[tokio::test] + async fn auth_required_status_unblocks_input_polling_loop() { + let repl = ReplChannel::with_user_id("test-user"); + repl.stdin_locked.store(true, Ordering::Relaxed); + + let stdin_locked = Arc::clone(&repl.stdin_locked); + let poller = std::thread::spawn(move || { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + while stdin_locked.load(Ordering::Relaxed) { + if std::time::Instant::now() >= deadline { + return false; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + true + }); + + repl.send_status( + StatusUpdate::AuthRequired { + extension_name: "google_oauth_token".to_string(), + instructions: Some("Paste your token".to_string()), + auth_url: None, + setup_url: Some("http://127.0.0.1:8080/auth".to_string()), + request_id: None, + }, + &serde_json::json!({}), + ) + .await + .expect("auth required status should render successfully"); + + let resumed = poller.join().expect("polling thread panicked"); + assert!( + resumed, + "input polling loop must observe the unlock so the next readline can start" + ); + assert!( + !repl.stdin_locked.load(Ordering::Relaxed), + "stdin_locked must remain false after AuthRequired so subsequent prompts aren't blocked" + ); + } }