vv-agent 0.4.8

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use serde_json::json;
use std::time::Duration;
use vv_agent::app_server::outgoing::OutgoingEnvelope;
use vv_agent::app_server::processor::MessageProcessor;
use vv_agent::app_server::protocol::{
    ApprovalDecision, ApprovalRequestParams, JsonRpcMessage, JsonRpcRequest, RequestId,
    ServerRequest,
};
use vv_agent::app_server::transport::ConnectionId;

fn request(id: i64, method: &str, params: serde_json::Value) -> JsonRpcMessage {
    JsonRpcMessage::Request(JsonRpcRequest {
        id: RequestId::Integer(id),
        method: method.to_string(),
        params: Some(params),
    })
}

fn initialize_request(id: i64) -> JsonRpcMessage {
    request(
        id,
        "initialize",
        json!({
            "clientInfo": {"name": "processor-method-test"},
            "capabilities": {}
        }),
    )
}

async fn initialize(
    processor: &mut MessageProcessor,
    outgoing: &mut tokio::sync::mpsc::Receiver<OutgoingEnvelope>,
    connection_id: ConnectionId,
) {
    processor
        .process_message(connection_id, initialize_request(1))
        .await;
    let _ = outgoing.recv().await.expect("initialize response");
}

async fn recv_for_connection(
    outgoing: &mut tokio::sync::mpsc::Receiver<OutgoingEnvelope>,
    connection_id: ConnectionId,
) -> JsonRpcMessage {
    loop {
        let envelope = outgoing.recv().await.expect("outgoing message");
        if envelope.connection_id == connection_id {
            return envelope.message;
        }
    }
}

#[tokio::test]
async fn schema_export_request_returns_json_and_typescript_bundles() {
    let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(16);
    let connection_id = ConnectionId::new(1);
    initialize(&mut processor, &mut outgoing, connection_id).await;

    processor
        .process_message(connection_id, request(2, "schema/export", json!({})))
        .await;

    let JsonRpcMessage::Response(response) =
        recv_for_connection(&mut outgoing, connection_id).await
    else {
        panic!("expected schema export response");
    };
    assert_eq!(response.id, RequestId::Integer(2));
    assert!(response.result["jsonSchema"]["ClientRequest"].is_string());
    assert!(response.result["jsonSchema"]["ServerNotification"].is_string());
    assert!(response.result["typescript"]["ClientRequest.ts"].is_string());
}

#[tokio::test]
async fn model_list_request_returns_a_valid_model_list_response() {
    let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(16);
    let connection_id = ConnectionId::new(1);
    initialize(&mut processor, &mut outgoing, connection_id).await;

    processor
        .process_message(connection_id, request(2, "model/list", json!({})))
        .await;

    let JsonRpcMessage::Response(response) =
        recv_for_connection(&mut outgoing, connection_id).await
    else {
        panic!("expected model list response");
    };
    assert_eq!(response.id, RequestId::Integer(2));
    assert!(response.result["models"].is_array());
}

#[tokio::test]
async fn approval_resolve_request_resolves_matching_server_request() {
    let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(16);
    let connection_id = ConnectionId::new(1);
    initialize(&mut processor, &mut outgoing, connection_id).await;

    let callback = processor
        .outgoing()
        .send_server_request_with_id(
            connection_id,
            RequestId::String("approval_1".to_string()),
            ServerRequest::ApprovalRequest(ApprovalRequestParams {
                thread_id: "thread_1".to_string(),
                turn_id: "turn_1".to_string(),
                request_id: "approval_1".to_string(),
                tool_name: "bash".to_string(),
                preview: "Run cargo test".to_string(),
                choices: vec![ApprovalDecision::Allow, ApprovalDecision::Deny],
            }),
        )
        .await
        .expect("server request");
    let _ = recv_for_connection(&mut outgoing, connection_id).await;

    processor
        .process_message(
            connection_id,
            request(
                2,
                "approval/resolve",
                json!({
                    "threadId": "thread_1",
                    "turnId": "turn_1",
                    "requestId": "approval_1",
                    "decision": "allow"
                }),
            ),
        )
        .await;

    let result = tokio::time::timeout(Duration::from_millis(100), callback)
        .await
        .expect("approval callback should resolve")
        .expect("callback")
        .expect("approval result");
    assert_eq!(result["decision"], "allow");
    let JsonRpcMessage::Response(response) =
        recv_for_connection(&mut outgoing, connection_id).await
    else {
        panic!("expected approval resolve response");
    };
    assert_eq!(response.id, RequestId::Integer(2));
}

#[tokio::test]
async fn turn_steer_returns_explicit_unsupported_error() {
    let (mut processor, mut outgoing) = MessageProcessor::new_for_tests(16);
    let connection_id = ConnectionId::new(1);
    initialize(&mut processor, &mut outgoing, connection_id).await;

    processor
        .process_message(
            connection_id,
            request(
                2,
                "turn/steer",
                json!({
                    "threadId": "thread_1",
                    "turnId": "turn_1",
                    "input": [{"text": "continue"}]
                }),
            ),
        )
        .await;

    let JsonRpcMessage::Error(error) = recv_for_connection(&mut outgoing, connection_id).await
    else {
        panic!("expected unsupported error");
    };
    assert_eq!(error.id, RequestId::Integer(2));
    assert_eq!(error.error.code, -32013);
    assert_eq!(error.error.data.expect("data")["method"], "turn/steer");
}