storkit: merge 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing
This commit is contained in:
@@ -616,7 +616,13 @@ pub(super) async fn handle_message(
|
||||
}
|
||||
Err(e) => {
|
||||
slog!("[matrix-bot] LLM error: {e}");
|
||||
let err_msg = format!("Error processing your request: {e}");
|
||||
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
|
||||
format!(
|
||||
"Authentication required. [Click here to log in to Claude]({url})"
|
||||
)
|
||||
} else {
|
||||
format!("Error processing your request: {e}")
|
||||
};
|
||||
let _ = msg_tx.send(err_msg.clone());
|
||||
(err_msg, None)
|
||||
}
|
||||
@@ -686,6 +692,24 @@ mod tests {
|
||||
assert_eq!(prompt, "@bob:example.com: What's up?");
|
||||
}
|
||||
|
||||
// -- OAuth login link formatting ----------------------------------------
|
||||
|
||||
#[test]
|
||||
fn oauth_error_produces_login_link() {
|
||||
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||
let url = crate::llm::oauth::extract_login_url_from_error(err);
|
||||
assert!(url.is_some(), "should extract URL from OAuth error");
|
||||
let msg = format!("Authentication required. [Click here to log in to Claude]({})", url.unwrap());
|
||||
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
|
||||
assert!(msg.contains("[Click here to log in to Claude]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_oauth_error_not_formatted_as_link() {
|
||||
let err = "Some unrelated error";
|
||||
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
|
||||
}
|
||||
|
||||
// -- bot_name / system prompt -------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -383,7 +383,13 @@ async fn handle_llm_message(ctx: &WhatsAppWebhookContext, sender: &str, user_mes
|
||||
}
|
||||
Err(e) => {
|
||||
slog!("[whatsapp] LLM error: {e}");
|
||||
let err_msg = format!("Error processing your request: {e}");
|
||||
let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
|
||||
format!(
|
||||
"Authentication required. Log in to Claude here: {url}"
|
||||
)
|
||||
} else {
|
||||
format!("Error processing your request: {e}")
|
||||
};
|
||||
let _ = msg_tx.send(err_msg.clone());
|
||||
(err_msg, None)
|
||||
}
|
||||
@@ -491,6 +497,24 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
// ── OAuth login link formatting ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn whatsapp_oauth_error_produces_plain_text_url() {
|
||||
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||
let url = crate::llm::oauth::extract_login_url_from_error(err);
|
||||
assert!(url.is_some(), "should extract URL from OAuth error");
|
||||
let msg = format!("Authentication required. Log in to Claude here: {}", url.unwrap());
|
||||
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
|
||||
assert!(!msg.contains('['), "WhatsApp message should not use Markdown link syntax");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whatsapp_non_oauth_error_not_formatted_as_link() {
|
||||
let err = "Some unrelated error occurred during processing";
|
||||
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
|
||||
}
|
||||
|
||||
// ── Allowlist tests ───────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -161,10 +161,43 @@ pub async fn refresh_access_token() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the OAuth login URL from an error message produced by the Claude Code provider.
|
||||
///
|
||||
/// The provider returns errors like:
|
||||
/// `"OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize"`
|
||||
///
|
||||
/// Returns the URL portion when the error indicates missing or expired credentials,
|
||||
/// `None` otherwise.
|
||||
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
|
||||
let marker = "Please log in: ";
|
||||
let start = err.find(marker)?;
|
||||
Some(err[start + marker.len()..].trim())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extract_login_url_from_oauth_error() {
|
||||
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
|
||||
let url = extract_login_url_from_error(err);
|
||||
assert_eq!(url, Some("http://localhost:3001/oauth/authorize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_login_url_returns_none_for_unrelated_error() {
|
||||
let err = "Some other error occurred";
|
||||
assert!(extract_login_url_from_error(err).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_login_url_with_different_port() {
|
||||
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3002/oauth/authorize";
|
||||
let url = extract_login_url_from_error(err);
|
||||
assert_eq!(url, Some("http://localhost:3002/oauth/authorize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_credentials_file() {
|
||||
let json = r#"{
|
||||
|
||||
@@ -138,9 +138,11 @@ impl ClaudeCodeProvider {
|
||||
on_token("\n*Refreshing authentication token...*\n");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
Err(_e) => {
|
||||
let port = crate::http::resolve_port();
|
||||
let login_url = format!("http://localhost:{port}/oauth/authorize");
|
||||
return Err(format!(
|
||||
"OAuth session expired. Please run `claude login` to re-authenticate. ({e})"
|
||||
"OAuth session expired or credentials missing. Please log in: {login_url}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user