rho-coding-agent 1.19.2

A lightweight agent harness inspired by Pi
Documentation
use pretty_assertions::assert_eq;

use std::sync::atomic::{AtomicUsize, Ordering};

use super::{
    resolve_claude_login_after_suspend, resolve_claude_login_auth_outcome, ClaudeLoginAfterSuspend,
    ClaudeLoginAuthOutcome, SignInTarget,
};
use crate::claude_runtime::auth::{self, ClaudeAuthError, ClaudeAuthStatus};

#[test]
fn sign_in_target_routes_claude_code_case_insensitively() {
    for value in ["claude-code", " Claude-Code "] {
        assert!(
            matches!(SignInTarget::parse(value), SignInTarget::ClaudeCode),
            "{value} must route to the external runtime"
        );
    }
    assert!(matches!(
        SignInTarget::parse(" anthropic "),
        SignInTarget::Provider(provider) if provider == "anthropic"
    ));
}

#[test]
fn handoff_and_logout_copy_keep_ownership_with_claude() {
    let handoff = auth::login_handoff_notice();
    assert!(handoff.contains("handing the terminal to the claude binary"));
    assert!(handoff.contains("Rho never sees or stores your token"));
    assert!(handoff.contains("/logout claude-code") || handoff.contains("claude auth logout"));

    let logout = auth::logout_confirm_description();
    assert!(logout.contains("everywhere the claude binary is used"));
    assert!(logout.contains("Rho does not store this credential"));
}

fn signed_in_status() -> ClaudeAuthStatus {
    ClaudeAuthStatus {
        logged_in: true,
        auth_method: None,
        api_provider: None,
        email: Some("someone@example.com".into()),
        org_id: None,
        org_name: None,
        subscription_type: Some("pro".into()),
    }
}

fn signed_out_status() -> ClaudeAuthStatus {
    ClaudeAuthStatus {
        logged_in: false,
        auth_method: None,
        api_provider: None,
        email: None,
        org_id: None,
        org_name: None,
        subscription_type: None,
    }
}

#[tokio::test]
async fn successful_login_with_signed_in_status_is_complete() {
    let outcome =
        resolve_claude_login_auth_outcome(Ok(()), || async { Ok(signed_in_status()) }).await;
    assert_eq!(outcome.status_line(), "claude code login complete");
    match outcome {
        ClaudeLoginAuthOutcome::Complete { notice } => {
            assert!(notice.contains("someone@example.com"));
        }
        other => panic!("expected complete outcome, got {other:?}"),
    }
}

#[tokio::test]
async fn successful_login_with_signed_out_status_is_incomplete() {
    let outcome =
        resolve_claude_login_auth_outcome(Ok(()), || async { Ok(signed_out_status()) }).await;
    assert_eq!(outcome.status_line(), "claude code login incomplete");
    match outcome {
        ClaudeLoginAuthOutcome::Incomplete { message } => {
            assert!(message.contains("status still reports signed out"));
        }
        other => panic!("expected incomplete outcome, got {other:?}"),
    }
}

#[tokio::test]
async fn successful_login_with_unreadable_status_is_incomplete() {
    let outcome =
        resolve_claude_login_auth_outcome(Ok(()), || async { Err(ClaudeAuthError::BinaryMissing) })
            .await;
    assert_eq!(outcome.status_line(), "claude code login incomplete");
    match outcome {
        ClaudeLoginAuthOutcome::Incomplete { message } => {
            assert!(message.contains("status could not be read"));
        }
        other => panic!("expected incomplete outcome, got {other:?}"),
    }
}

#[tokio::test]
async fn failed_login_still_complete_when_status_shows_signed_in() {
    let outcome = resolve_claude_login_auth_outcome(
        Err(anyhow::anyhow!(
            "claude auth login exited with exit status: 1"
        )),
        || async { Ok(signed_in_status()) },
    )
    .await;
    assert_eq!(outcome.status_line(), "claude code login complete");
    match outcome {
        ClaudeLoginAuthOutcome::Complete { notice } => {
            assert!(notice.contains("status shows signed in"));
        }
        other => panic!("expected complete outcome, got {other:?}"),
    }
}

#[tokio::test]
async fn failed_login_with_signed_out_status_is_failed() {
    let outcome = resolve_claude_login_auth_outcome(
        Err(anyhow::anyhow!(
            "claude auth login exited with exit status: 1"
        )),
        || async { Ok(signed_out_status()) },
    )
    .await;
    assert_eq!(outcome.status_line(), "claude code login failed");
    match outcome {
        ClaudeLoginAuthOutcome::Failed { message } => {
            assert!(message.contains("login failed"));
        }
        other => panic!("expected failed outcome, got {other:?}"),
    }
}

#[tokio::test]
async fn resume_failure_does_not_query_auth_status() {
    let query_calls = AtomicUsize::new(0);
    let result = resolve_claude_login_after_suspend(
        Err(anyhow::anyhow!(
            "failed to resume Rho after suspended operation"
        )),
        Ok(()),
        || {
            query_calls.fetch_add(1, Ordering::SeqCst);
            async { Ok(signed_in_status()) }
        },
    )
    .await;

    assert_eq!(query_calls.load(Ordering::SeqCst), 0);
    match result {
        ClaudeLoginAfterSuspend::ResumeFailed { error } => {
            let rendered = format!("{error:#}");
            assert!(
                rendered.contains("failed to resume Rho after suspended operation"),
                "{rendered}"
            );
            assert!(!rendered.contains("post-login auth status"), "{rendered}");
        }
        ClaudeLoginAfterSuspend::AuthResolved { outcome } => {
            panic!("expected resume failure, got auth outcome {outcome:?}");
        }
    }
}

#[tokio::test]
async fn resume_failure_keeps_restore_primary_and_attaches_child_error() {
    let query_calls = AtomicUsize::new(0);
    let result = resolve_claude_login_after_suspend(
        Err(anyhow::anyhow!(
            "failed to resume Rho after suspended operation"
        )),
        Err(anyhow::anyhow!(
            "claude auth login exited with exit status: 1"
        )),
        || {
            query_calls.fetch_add(1, Ordering::SeqCst);
            async { Ok(signed_out_status()) }
        },
    )
    .await;

    assert_eq!(query_calls.load(Ordering::SeqCst), 0);
    match result {
        ClaudeLoginAfterSuspend::ResumeFailed { error } => {
            let rendered = format!("{error:#}");
            assert!(
                rendered.contains("failed to resume Rho after suspended operation"),
                "{rendered}"
            );
            assert!(
                rendered.contains("claude auth login also failed"),
                "{rendered}"
            );
            assert!(
                rendered.contains("claude auth login exited with exit status: 1"),
                "{rendered}"
            );
        }
        ClaudeLoginAfterSuspend::AuthResolved { outcome } => {
            panic!("expected resume failure, got auth outcome {outcome:?}");
        }
    }
}

#[tokio::test]
async fn successful_resume_queries_auth_after_child_success() {
    let query_calls = AtomicUsize::new(0);
    let result = resolve_claude_login_after_suspend(Ok(()), Ok(()), || {
        query_calls.fetch_add(1, Ordering::SeqCst);
        async { Ok(signed_in_status()) }
    })
    .await;

    assert_eq!(query_calls.load(Ordering::SeqCst), 1);
    match result {
        ClaudeLoginAfterSuspend::AuthResolved { outcome } => {
            assert_eq!(outcome.status_line(), "claude code login complete");
        }
        ClaudeLoginAfterSuspend::ResumeFailed { error } => {
            panic!("expected auth outcome, got resume failure {error:#}");
        }
    }
}

#[tokio::test]
async fn successful_resume_queries_auth_after_child_failure() {
    let query_calls = AtomicUsize::new(0);
    let result = resolve_claude_login_after_suspend(
        Ok(()),
        Err(anyhow::anyhow!(
            "claude auth login exited with exit status: 1"
        )),
        || {
            query_calls.fetch_add(1, Ordering::SeqCst);
            async { Ok(signed_in_status()) }
        },
    )
    .await;

    assert_eq!(query_calls.load(Ordering::SeqCst), 1);
    match result {
        ClaudeLoginAfterSuspend::AuthResolved { outcome } => {
            assert_eq!(outcome.status_line(), "claude code login complete");
            match outcome {
                ClaudeLoginAuthOutcome::Complete { notice } => {
                    assert!(notice.contains("status shows signed in"));
                }
                other => panic!("expected complete outcome, got {other:?}"),
            }
        }
        ClaudeLoginAfterSuspend::ResumeFailed { error } => {
            panic!("expected auth outcome, got resume failure {error:#}");
        }
    }
}