wordval 1.0.4

Wordval: A fast, simple, and productive Rust web framework - config-driven, plugin-extensible, batteries included
//! `#[tools]` 宏集成测试:宏展开 + 工具执行 + MCP server 暴露
//!
//! 覆盖场景:
//! - 与 `#[post]` 路由宏叠加(同一函数既是 HTTP handler 又是 MCP 工具)
//! - `Json<T>` 提取器参数:schema 属性合并进顶层 + args 整体反序列化
//! - 基础类型参数(必需 / Option 可选)
//! - 返回类型映射:`ResResult<JsonValue>`(data 序列化)、`Res<JsonValue>`(直接返回)
//! - MCP server 端点:initialize / tools/list / tools/call / 错误路径

use wordval::prelude::*;
use wordval::serde_json::{json, Value as JsonValue};

// tests 是独立 crate:axum 经 wordval 主 crate re-export 访问
use wordval::axum;

/// 创建用户请求体(业务结构体需手动实现 JsonSchema)
#[derive(Debug, serde::Deserialize)]
struct CreateUserReq {
    name: String,
    email: Option<String>,
}

impl wordval::wordval_agent::tools::schema::JsonSchema for CreateUserReq {
    fn json_schema() -> JsonValue {
        json!({
            "type": "object",
            "properties": {
                "name": { "type": "string" },
                "email": { "type": "string" },
            },
            "required": ["name"],
        })
    }
}

/// 创建用户:既是 HTTP 接口(`#[post]`),又是 MCP 工具(`#[tools]`)
#[wordval::post("/api/user/create")]
#[wordval::tools]
async fn create_user(Json(req): Json<CreateUserReq>) -> ResResult<JsonValue> {
    Ok(Res::ok(json!({"id": 1, "name": req.name})))
}

/// 查询用户:基础类型参数 + Res 直接返回 + description 属性
#[wordval::tools(description = "按 ID 查询用户")]
async fn get_user(id: u64, verbose: Option<bool>) -> Res<JsonValue> {
    if verbose.unwrap_or(false) {
        Res::ok(json!({"id": id, "detail": "full"}))
    } else {
        Res::ok(json!({"id": id}))
    }
}

fn find_tool(name: &str) -> Box<dyn wordval::wordval_agent::tools::Tool> {
    wordval::wordval_agent::tools::registry::registered_tools()
        .into_iter()
        .find(|t| t.name() == name)
        .unwrap_or_else(|| panic!("工具 {name} 未注册"))
}

fn ctx() -> wordval::wordval_agent::tools::ToolCtx {
    wordval::wordval_agent::tools::ToolCtx::new(std::env::temp_dir(), 0)
}

#[test]
fn macro_keeps_original_fn() {
    // 编译期验证:原函数保留(可被路由宏引用),签名不变
    let _ = create_user;
    let _ = get_user;
}

#[test]
fn schema_generated() {
    // Json<CreateUserReq>:object 属性合并进工具顶层参数
    let s = find_tool("create_user").parameters_schema();
    assert_eq!(s["properties"]["name"]["type"], "string");
    assert_eq!(s["properties"]["email"]["type"], "string");
    assert!(s["required"].as_array().unwrap().contains(&json!("name")));

    // 基础类型:按参数名生成;Option 不进 required
    let g = find_tool("get_user");
    assert_eq!(g.description(), "按 ID 查询用户");
    let gs = g.parameters_schema();
    assert_eq!(gs["properties"]["id"]["type"], "integer");
    assert!(gs["required"].as_array().unwrap().contains(&json!("id")));
    assert!(!gs["required"]
        .as_array()
        .unwrap()
        .contains(&json!("verbose")));
}

#[tokio::test]
async fn execute_ok() {
    // Json<T> 从 args 整体反序列化
    let r = find_tool("create_user")
        .execute(&json!({ "name": "张三" }), &ctx())
        .await;
    assert!(!r.is_error, "{}", r.content);
    assert_eq!(r.content, r#"{"id":1,"name":"张三"}"#);

    // 基础类型 + Option 参数
    let g = find_tool("get_user");
    let r = g.execute(&json!({ "id": 7 }), &ctx()).await;
    assert!(!r.is_error, "{}", r.content);
    assert_eq!(r.content, r#"{"id":7}"#);
    let r = g
        .execute(&json!({ "id": 7, "verbose": true }), &ctx())
        .await;
    assert!(!r.is_error, "{}", r.content);
    assert_eq!(r.content, r#"{"detail":"full","id":7}"#);
}

#[tokio::test]
async fn execute_errors() {
    let g = find_tool("get_user");
    // 缺必需参数
    let r = g.execute(&json!({}), &ctx()).await;
    assert!(
        r.is_error && r.content.contains("缺少必需参数: id"),
        "{}",
        r.content
    );
    // 类型错误
    let r = g.execute(&json!({ "id": "x" }), &ctx()).await;
    assert!(
        r.is_error && r.content.contains("解析失败"),
        "{}",
        r.content
    );
}

// ── MCP server 端点 ─────────────────────────────────────

async fn post_json(app: &axum::Router, payload: JsonValue) -> (axum::http::StatusCode, JsonValue) {
    use axum::body::Body;
    use http_body_util::BodyExt;
    use tower::ServiceExt;
    let resp = app
        .clone()
        .oneshot(
            axum::http::Request::builder()
                .method("POST")
                .uri("/")
                .header("content-type", "application/json")
                .body(Body::from(payload.to_string()))
                .unwrap(),
        )
        .await
        .unwrap();
    let status = resp.status();
    let bytes = resp.into_body().collect().await.unwrap().to_bytes();
    let v = if bytes.is_empty() {
        json!({})
    } else {
        wordval::serde_json::from_slice(&bytes).unwrap()
    };
    (status, v)
}

#[tokio::test]
async fn mcp_initialize() {
    let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
    let (status, v) = post_json(&app, json!({
        "jsonrpc": "2.0", "id": 1, "method": "initialize",
        "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "test" } }
    }))
    .await;
    assert_eq!(status, 200);
    assert_eq!(v["id"], 1);
    assert_eq!(v["result"]["protocolVersion"], "2025-03-26");
    assert_eq!(v["result"]["serverInfo"]["name"], "wordval");
}

#[tokio::test]
async fn mcp_tools_list_includes_macro_tools() {
    let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
    let (_, v) = post_json(
        &app,
        json!({
            "jsonrpc": "2.0", "id": 2, "method": "tools/list"
        }),
    )
    .await;
    let tools = v["result"]["tools"].as_array().unwrap();
    let create = tools
        .iter()
        .find(|t| t["name"] == "create_user")
        .expect("MCP tools/list 应包含 create_user");
    assert_eq!(
        create["inputSchema"]["properties"]["name"]["type"],
        "string"
    );
    let get = tools
        .iter()
        .find(|t| t["name"] == "get_user")
        .expect("MCP tools/list 应包含 get_user");
    assert_eq!(get["description"], "按 ID 查询用户");
}

#[tokio::test]
async fn mcp_tools_call() {
    let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
    let (_, v) = post_json(
        &app,
        json!({
            "jsonrpc": "2.0", "id": 3, "method": "tools/call",
            "params": { "name": "get_user", "arguments": { "id": 9 } }
        }),
    )
    .await;
    assert_eq!(v["result"]["content"][0]["type"], "text");
    assert_eq!(v["result"]["content"][0]["text"], r#"{"id":9}"#);
    assert_eq!(v["result"]["isError"], false);

    // 未知工具 → JSON-RPC 错误
    let (_, v) = post_json(
        &app,
        json!({
            "jsonrpc": "2.0", "id": 4, "method": "tools/call",
            "params": { "name": "no_such_tool", "arguments": {} }
        }),
    )
    .await;
    assert_eq!(v["error"]["code"], -32602);
}

#[tokio::test]
async fn mcp_unknown_method_and_notification() {
    let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
    // 未知方法
    let (_, v) = post_json(
        &app,
        json!({
            "jsonrpc": "2.0", "id": 5, "method": "bogus/method"
        }),
    )
    .await;
    assert_eq!(v["error"]["code"], -32601);

    // notification(无 id):202 Accepted + 空 body
    let (status, v) = post_json(
        &app,
        json!({
            "jsonrpc": "2.0", "method": "notifications/initialized"
        }),
    )
    .await;
    assert_eq!(status, 202);
    assert_eq!(v, json!({}));
}