crabmate 0.4.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
use reqwest::Url;

use super::decode::{apply_text_format_if_requested, decode_http_body_text_for_tool};
use super::{
    FetchMethod, HttpBodyTextFormat, RequestMethod, approval_args_display,
    approval_args_display_request, html_to_readable_text, parse_http_fetch_args,
    parse_http_request_args, request_storage_key, url_matches_allowed_prefixes,
};

#[test]
fn parse_defaults_to_get() {
    let (u, m, fmt) = parse_http_fetch_args(r#"{"url":"https://example.com/a"}"#).unwrap();
    assert_eq!(m, FetchMethod::Get);
    assert_eq!(fmt, HttpBodyTextFormat::Raw);
    assert_eq!(u.as_str(), "https://example.com/a");
}

#[test]
fn parse_head() {
    let (u, m, fmt) =
        parse_http_fetch_args(r#"{"url":"https://example.com/","method":"head"}"#).unwrap();
    assert_eq!(m, FetchMethod::Head);
    assert_eq!(fmt, HttpBodyTextFormat::Raw);
    assert_eq!(u.host_str(), Some("example.com"));
}

#[test]
fn parse_html_text_format() {
    let (_, _, fmt) =
        parse_http_fetch_args(r#"{"url":"https://example.com/","text_format":"html_text"}"#)
            .unwrap();
    assert_eq!(fmt, HttpBodyTextFormat::HtmlText);
}

#[test]
fn approval_display_head() {
    let u = Url::parse("https://ex.com/x?q=1").unwrap();
    let s = approval_args_display(FetchMethod::Head, &u);
    assert!(s.starts_with("HEAD "));
    assert!(s.contains("ex.com/x"));
    assert!(!s.contains("q=1"), "query 应被脱敏: {}", s);
}

#[test]
fn request_storage_key_includes_method_and_strips_query() {
    let u = Url::parse("https://ex.com/a?x=1").unwrap();
    let k = request_storage_key(RequestMethod::Post, &u);
    assert_eq!(k, "http_request:POST:https://ex.com/a");
}

#[test]
fn approval_display_request_redacts_and_notes_body() {
    let u = Url::parse("https://ex.com/x?q=1").unwrap();
    let s = approval_args_display_request(RequestMethod::Put, &u, true);
    assert!(s.starts_with("PUT "));
    assert!(s.contains("(含 json_body)"));
    assert!(!s.contains("q=1"));
}

#[test]
fn allowed_prefix_requires_origin_and_path_boundary() {
    let url = Url::parse("https://example.com/api/v1/users").unwrap();
    assert!(url_matches_allowed_prefixes(
        &url,
        &["https://example.com/api/".to_string()]
    ));
    assert!(url_matches_allowed_prefixes(
        &url,
        &["https://example.com/api".to_string()]
    ));
    assert!(!url_matches_allowed_prefixes(
        &url,
        &["https://example.com/ap".to_string()]
    ));
    assert!(!url_matches_allowed_prefixes(
        &url,
        &["https://example.com/api2/".to_string()]
    ));
    assert!(!url_matches_allowed_prefixes(
        &url,
        &["https://example.comx/api/".to_string()]
    ));
}
#[test]
fn parse_http_request_supports_patch_with_body() {
    let (u, m, body, fmt) = parse_http_request_args(
        r#"{"url":"https://example.com/api","method":"patch","json_body":{"x":1}}"#,
    )
    .unwrap();
    assert_eq!(u.as_str(), "https://example.com/api");
    assert_eq!(m, RequestMethod::Patch);
    assert_eq!(fmt, HttpBodyTextFormat::Raw);
    let b = body.unwrap();
    assert_eq!(b.get("x").and_then(|x| x.as_i64()), Some(1));
}

#[test]
fn parse_http_request_rejects_get() {
    let err =
        parse_http_request_args(r#"{"url":"https://example.com","method":"GET"}"#).unwrap_err();
    assert!(err.contains("POST/PUT/PATCH/DELETE"));
}

#[test]
fn decode_body_uses_content_type_charset() {
    let gbk = encoding_rs::GBK;
    let bytes = gbk.encode("你好").0;
    let (text, note) = decode_http_body_text_for_tool("text/plain; charset=gbk", bytes.as_ref());
    assert_eq!(text, "你好");
    assert!(note.to_ascii_lowercase().contains("gbk"), "note={}", note);
}

#[test]
fn decode_body_html_meta_charset() {
    let html =
        b"<!DOCTYPE html><html><head><meta charset=\"gbk\"></head><body>\xc4\xe3\xba\xc3</body>";
    let (text, note) = decode_http_body_text_for_tool("text/html", html);
    assert!(text.contains("你好"), "text={}", text);
    assert!(
        note.contains("meta") || note.to_ascii_lowercase().contains("gbk"),
        "note={}",
        note
    );
}

#[test]
fn decode_body_json_utf8_strict() {
    let (text, note) = decode_http_body_text_for_tool("application/json", br#"{"a":1}"#);
    assert_eq!(text, r#"{"a":1}"#);
    assert!(note.contains("UTF-8"), "note={}", note);
}

#[test]
fn decode_body_xml_declaration() {
    let xml = b"<?xml version=\"1.0\" encoding=\"GBK\"?><r>\xc4\xe3\xba\xc3</r>";
    let (text, note) = decode_http_body_text_for_tool("application/xml", xml);
    assert!(text.contains("你好"), "text={}", text);
    assert!(note.contains("XML"), "note={}", note);
}

#[test]
fn html_to_text_strips_script_and_collapses() {
    let html = r#"<!DOCTYPE html><html><head><title>x</title></head><body>
            <script>alert(1)</script>
            <p>Hello <b>world</b></p>
            <style>.x{}</style>
        </body></html>"#;
    let t = html_to_readable_text(html).unwrap();
    assert!(t.contains("Hello"));
    assert!(t.contains("world"));
    assert!(!t.contains("script"));
    assert!(!t.contains("alert"));
}

#[test]
fn apply_html_text_format_on_html() {
    let html = "<html><body><p>Hi</p></body></html>";
    let (body, note) = apply_text_format_if_requested(
        "text/html",
        HttpBodyTextFormat::HtmlText,
        html.to_string(),
        "正文解码: UTF-8(测试)".to_string(),
    );
    assert_eq!(body, "Hi");
    assert!(note.contains("HTML→纯文本"), "note={}", note);
}