fix(repl): unlock stdin after auth prompt (#2640)

* 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 <noreply@anthropic.com>
This commit is contained in:
firat.sertgoz
2026-04-18 16:32:28 +03:00
committed by GitHub
parent 1b99d0c325
commit e25568437f

View File

@@ -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"
);
}
}