rho-coding-agent 1.6.0

A lightweight agent harness inspired by Pi
Documentation
use super::*;
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpListener,
};

#[tokio::test]
async fn device_authorization_posts_form_client_id_and_parses_response() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let endpoint = format!(
        "http://{}/api/oauth/device_authorization",
        listener.local_addr().unwrap()
    );
    let server = tokio::spawn(async move {
        let (mut stream, _) = listener.accept().await.unwrap();
        let mut request = vec![0; 4096];
        let read = stream.read(&mut request).await.unwrap();
        let request = String::from_utf8_lossy(&request[..read]);
        assert!(request.starts_with("POST /api/oauth/device_authorization HTTP/1.1"));
        assert!(request.contains("application/x-www-form-urlencoded"));
        assert!(request.contains(&format!("client_id={CLIENT_ID}")));
        let body = r#"{"device_code":"device-secret","user_code":"ABCD-EFGH","verification_uri":"https://auth.kimi.com/device","verification_uri_complete":"https://auth.kimi.com/device?code=secret","expires_in":300,"interval":5}"#;
        let response = format!(
            "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
            body.len()
        );
        stream.write_all(response.as_bytes()).await.unwrap();
    });

    let login = start_with_endpoint(&reqwest::Client::new(), &endpoint)
        .await
        .unwrap();
    assert_eq!(login.user_code, "ABCD-EFGH");
    assert_eq!(login.verification_uri, "https://auth.kimi.com/device");
    server.await.unwrap();
}

#[tokio::test]
async fn refresh_posts_form_and_returns_rotated_tokens() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let endpoint = format!("http://{}/api/oauth/token", listener.local_addr().unwrap());
    let server = tokio::spawn(async move {
        let (mut stream, _) = listener.accept().await.unwrap();
        let mut request = vec![0; 4096];
        let read = stream.read(&mut request).await.unwrap();
        let request = String::from_utf8_lossy(&request[..read]);
        assert!(request.starts_with("POST /api/oauth/token HTTP/1.1"));
        assert!(request.contains("grant_type=refresh_token"));
        assert!(request.contains("refresh_token=old-refresh"));
        assert!(request.contains(&format!("client_id={CLIENT_ID}")));
        let body = r#"{"access_token":"new-access","refresh_token":"new-refresh","expires_in":3600,"scope":"kimi:code","token_type":"Bearer"}"#;
        let response = format!(
            "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
            body.len()
        );
        stream.write_all(response.as_bytes()).await.unwrap();
    });

    let tokens =
        refresh_kimi_tokens_with_endpoint(&reqwest::Client::new(), "old-refresh", &endpoint)
            .await
            .unwrap();
    assert_eq!(tokens.access_token, "new-access");
    assert_eq!(tokens.refresh_token.as_deref(), Some("new-refresh"));
    assert_eq!(tokens.scope, "kimi:code");
    assert_eq!(tokens.token_type, "Bearer");
    assert_eq!(tokens.expires_in, Some(3_600));
    assert!(tokens
        .expires_at_unix
        .is_some_and(|expires| expires > now_unix()));
    server.await.unwrap();
}

#[tokio::test]
async fn refresh_retries_rate_limits_and_accepts_rotated_tokens() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let endpoint = format!("http://{}/api/oauth/token", listener.local_addr().unwrap());
    let server = tokio::spawn(async move {
        for (status, body) in [
            (
                "429 Too Many Requests",
                r#"{"error":"temporarily_unavailable"}"#,
            ),
            (
                "200 OK",
                r#"{"access_token":"new-access","refresh_token":"new-refresh","expires_in":3600}"#,
            ),
        ] {
            let (mut stream, _) = listener.accept().await.unwrap();
            let mut request = vec![0; 4096];
            let bytes = stream.read(&mut request).await.unwrap();
            assert!(bytes > 0);
            let response = format!(
                "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
                body.len()
            );
            stream.write_all(response.as_bytes()).await.unwrap();
        }
    });

    let tokens =
        refresh_kimi_tokens_with_endpoint(&reqwest::Client::new(), "old-refresh", &endpoint)
            .await
            .unwrap();

    assert_eq!(tokens.access_token, "new-access");
    server.await.unwrap();
}

#[test]
fn debug_redacts_device_codes_and_complete_uri() {
    let login = KimiDeviceLogin {
        user_code: "user-secret".into(),
        verification_uri: "https://auth.kimi.com/device".into(),
        verification_uri_complete: Some("https://auth.kimi.com/device?secret".into()),
        device_code: "device-secret".into(),
        expires_in: Duration::from_secs(300),
        interval: Duration::from_secs(5),
    };
    let debug = format!("{login:?}");
    assert!(!debug.contains("user-secret"));
    assert!(!debug.contains("device-secret"));
    assert!(!debug.contains("?secret"));
}