gpui-form 0.7.0

The gpui-form crate
Documentation
use super::*;

#[cfg(feature = "runtime")]
#[test]
fn component_backed_field_decodes_through_shape_schema() {
    let mut server = test_server();
    form::<ComponentRequest>(&mut server)
        .model(|request| {
            Ok::<ObjectResponse, String>(object_response(json!({ "title": request.title })))
        })
        .expect("tool should register");

    let result = server.call_tool(
        &ComponentRequest::descriptor().tool_name(),
        Some(json!({ "title": "Headless" })),
    );

    assert_eq!(result.is_error, Some(false));
    assert_eq!(
        result.structured_content,
        Some(json!({ "title": "Headless" }))
    );
}

#[cfg(feature = "runtime")]
#[test]
fn editor_tools_patch_component_backed_fields_through_shape_schema() {
    let mut server = test_server();
    form::<ComponentRequest>(&mut server)
        .editor()
        .expect("component-backed editor tools should register");
    let tools = editor_tool_names(ComponentRequest::descriptor());
    let patch_tool = server
        .list_tools()
        .into_iter()
        .find(|tool| tool.name == tools.patch)
        .expect("patch tool should be registered");
    assert_eq!(
        patch_tool.input_schema["properties"]["values"]["properties"]["title"]["type"],
        "string"
    );
    assert_eq!(
        patch_tool.input_schema["properties"]["clear"]["items"]["enum"],
        json!(["title"])
    );
    assert_eq!(
        patch_tool.input_schema["properties"]["replace"]["type"],
        "boolean"
    );

    let open = server.call_tool(
        &tools.open,
        Some(json!({
            "values": {
                "title": "Component title"
            }
        })),
    );
    assert_eq!(open.is_error, Some(false));
    let opened = open
        .structured_content
        .expect("open should return a session");
    let session_id = opened["session_id"]
        .as_str()
        .expect("session id should be a string");
    assert_eq!(opened["values"]["title"], "Component title");
    assert_eq!(opened["missing_required"], json!([]));
    assert_eq!(opened["valid"], true);

    let patched = server.call_tool(
        &tools.patch,
        Some(json!({
            "session_id": session_id,
            "values": {
                "title": "Updated component title"
            }
        })),
    );
    assert_eq!(patched.is_error, Some(false));
    assert_eq!(
        patched.structured_content.expect("patch")["submit_arguments"],
        json!({ "title": "Updated component title" })
    );
}

#[test]
fn generated_mcp_submit_uses_field_mcp_tool_value_decoder() {
    let schema = CustomToolValueRequest::descriptor().input_schema();
    assert_eq!(schema["properties"]["tags"]["type"], "string");

    let mut server = test_server();
    form::<CustomToolValueRequest>(&mut server)
        .model(|request| {
            Ok::<ObjectResponse, String>(object_response(json!({ "tags": request.tags.0 })))
        })
        .expect("tool should register with the field McpToolValue impl");

    let result = server.call_tool(
        &CustomToolValueRequest::descriptor().tool_name(),
        Some(json!({ "tags": "alpha / beta / gamma" })),
    );

    assert_eq!(result.is_error, Some(false));
    assert_eq!(
        result.structured_content,
        Some(json!({ "tags": ["alpha", "beta", "gamma"] }))
    );

    let result = server.call_tool(
        &CustomToolValueRequest::descriptor().tool_name(),
        Some(json!({ "tags": ["alpha"] })),
    );

    assert_eq!(result.is_error, Some(true));
    assert!(
        result.content[0]
            .as_text()
            .is_some_and(|text| text.text.contains("expected slash-separated tags"))
    );
    assert_eq!(
        result.structured_content.expect("structured error")["error"],
        json!({
            "kind": "decode_field",
            "message": "failed to decode field `tags`: expected slash-separated tags",
            "field": "tags",
            "detail": "expected slash-separated tags"
        })
    );
}

#[test]
fn generated_mcp_submit_reuses_mcp_tool_input_as_field_schema() {
    let mut server = test_server();
    form::<ToolInputSchemaRequest>(&mut server)
        .model(|request| {
            Ok::<ObjectResponse, String>(object_response(json!({
                "email_updates": request.preferences.email_updates,
                "topics": request.preferences.topics,
            })))
        })
        .expect("tool should register with the field McpToolInput-derived schema");

    let result = server.call_tool(
        &ToolInputSchemaRequest::descriptor().tool_name(),
        Some(json!({
            "preferences": {
                "email": true,
                "topics": ["rust", "gpui"]
            }
        })),
    );

    assert_eq!(result.is_error, Some(false));
    assert_eq!(
        result.structured_content,
        Some(json!({
            "email_updates": true,
            "topics": ["rust", "gpui"]
        }))
    );

    let result = server.call_tool(
        &ToolInputSchemaRequest::descriptor().tool_name(),
        Some(json!({
            "preferences": {
                "email": true,
                "unexpected": true
            }
        })),
    );

    assert_eq!(result.is_error, Some(true));
    assert!(matches!(
        result.content[0].as_text(),
        Some(text) if text.text.contains("unknown field `preferences.unexpected`")
    ));
    assert_eq!(
        result.structured_content.expect("structured error")["error"],
        json!({
            "kind": "unknown_field",
            "message": "unknown field `preferences.unexpected`",
            "field": "preferences.unexpected"
        })
    );
}

#[cfg(feature = "runtime")]
#[test]
fn component_backed_field_uses_typed_schema_when_shape_input_is_unavailable() {
    let schema = ComponentNoSchemaRequest::descriptor().input_schema();
    assert_eq!(schema["properties"]["title"]["type"], "string");
    assert_eq!(
        schema["properties"]["title"]["x-gpuiFormComponentBacked"],
        true
    );

    let mut server = test_server();
    form::<ComponentNoSchemaRequest>(&mut server)
        .model(|request| {
            Ok::<ObjectResponse, String>(object_response(json!({ "title": request.title.0 })))
        })
        .expect("tool should register with the field McpJsonSchema impl");

    let result = server.call_tool(
        &ComponentNoSchemaRequest::descriptor().tool_name(),
        Some(json!({ "title": "Headless" })),
    );

    assert_eq!(result.is_error, Some(false));
    assert_eq!(
        result.structured_content,
        Some(json!({ "title": "Headless" }))
    );
}

#[test]
fn submit_only_mcp_forms_do_not_require_serializable_field_values() {
    let mut server = test_server();
    form::<DecodeOnlyRequest>(&mut server)
        .model(|request| {
            Ok::<ObjectResponse, String>(object_response(json!({ "value": request.value.0 })))
        })
        .expect("submit tool should register without editor serialization bounds");

    let result = server.call_tool(
        &DecodeOnlyRequest::descriptor().tool_name(),
        Some(json!({ "value": "decode-only" })),
    );

    assert_eq!(result.is_error, Some(false));
    assert_eq!(
        result.structured_content,
        Some(json!({ "value": "decode-only" }))
    );
}