fix(stockbit): tell a polling agent when Stockbit refuses the sign-in

A refused cookie in idx's browser was only a stderr warning, which nobody
reads under idx-mcp: stockbit_status showed `waiting` until the timeout, and
the timeout message did not mention the refusal.

- runtime::on_warning sends a thread's warnings to a sink as well as
  stderr, until its guard drops.
- The background sign-in puts its warnings in `sign_in.warning`, cleared
  once it signs in.
- The errors that end a browser wait (timeout, window closed, browser
  exited) name a refusal it hit.

Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY
This commit is contained in:
Rasyidan Akbar F. 2026-11-04 19:22:14 +07:00
commit 362d314118
8 changed files with 130 additions and 18 deletions

View file

@ -234,6 +234,9 @@ fn wait_for_session(
) -> Result<(), IdxError> {
let deadline = Instant::now() + timeout;
let mut rejected: Option<String> = None;
// Why Stockbit refused that cookie, so the error that ends the wait says
// so: a polling frontend never saw the warning.
let mut refusal: Option<String> = None;
// After a check fails for a passing reason, the next is held back.
let mut retry_at: Option<Instant> = None;
let mut last_error: Option<String> = None;
@ -273,6 +276,7 @@ fn wait_for_session(
"Stockbit refused the session in idx's browser ({err}); sign in \
again in that window"
));
refusal = Some(err.to_string());
rejected = Some(cookie.value);
retry_at = None;
}
@ -283,12 +287,18 @@ fn wait_for_session(
Ok(_) => last_error = None,
Err(err) => last_error = Some(err.to_string()),
}
let refused = || {
refusal
.as_ref()
.map(|err| format!("; Stockbit refused the session signed in there ({err})"))
.unwrap_or_default()
};
if !alive() {
devtools.close();
return Err(IdxError::Io(
"the browser exited before idx got a session; run `idx stockbit login` again"
.to_string(),
));
return Err(IdxError::Io(format!(
"the browser exited before idx got a session{}; run `idx stockbit login` again",
refused()
)));
}
match devtools.has_page() {
Ok(true) => {
@ -299,11 +309,11 @@ fn wait_for_session(
misses += 1;
if saw_window && misses >= 2 {
devtools.close();
return Err(IdxError::Io(
"the Stockbit sign-in window was closed before the session arrived; \
run `idx stockbit login` again"
.to_string(),
));
return Err(IdxError::Io(format!(
"the Stockbit sign-in window was closed before the session arrived{}; \
run `idx stockbit login` again",
refused()
)));
}
}
Err(err) => last_error = Some(err.to_string()),
@ -311,9 +321,10 @@ fn wait_for_session(
if Instant::now() >= deadline {
devtools.close();
let mut message = format!(
"the Stockbit sign-in timed out after {}s and idx closed its browser window; \
"the Stockbit sign-in timed out after {}s and idx closed its browser window{}; \
re-run `idx stockbit login`, or add --no-browser for the local page",
timeout.as_secs()
timeout.as_secs(),
refused()
);
if let Some(err) = last_error {
message.push_str(&format!(" (last DevTools error: {err})"));
@ -993,6 +1004,11 @@ mod tests {
)
.unwrap_err();
assert!(err.to_string().contains("timed out"), "{err}");
assert!(
err.to_string()
.contains("Stockbit refused the session signed in there (auth error: expired)"),
"{err}"
);
assert_eq!(checks.get(), 1, "one refusal per distinct cookie");
browser.finish();
let _ = std::fs::remove_dir_all(&profile);