#![cfg(feature = "acp-experimental")]
use proptest::prelude::*;
use protocol_droid::domain::acp::implementation::Implementation;
use protocol_droid::domain::acp::initialize::{
AgentCapabilities, ClientCapabilities, InitializeRequest, InitializeResponse,
};
use protocol_droid::domain::acp::method::AcpMethod;
use protocol_droid::domain::acp::protocol_version::AcpProtocolVersion;
const ACP_SCHEMA_JSON: &str =
include_str!("../../protocol-droid-schema/spec/acp/v1.25.0/schema.json");
fn arbitrary_initialize_request() -> impl Strategy<Value = InitializeRequest> {
(
any::<bool>(), any::<bool>(), )
.prop_map(|(with_info, non_default_info)| InitializeRequest {
protocol_version: AcpProtocolVersion::CURRENT,
client_capabilities: ClientCapabilities::default(),
client_info: if with_info {
Some(if non_default_info {
Implementation::protocol_droid("protocol-droid-test", "1.2.3")
} else {
Implementation::protocol_droid("protocol-droid-test", "0.0.0")
})
} else {
None
},
})
}
fn arbitrary_initialize_response() -> impl Strategy<Value = InitializeResponse> {
any::<bool>().prop_map(|with_agent_info| InitializeResponse {
protocol_version: AcpProtocolVersion::CURRENT,
agent_capabilities: AgentCapabilities::default(),
auth_methods: None,
agent_info: if with_agent_info {
Some(Implementation::protocol_droid("reference-agent", "0.0.1"))
} else {
None
},
})
}
fn arbitrary_acp_method() -> impl Strategy<Value = AcpMethod> {
Just(AcpMethod::Agent(
protocol_droid::domain::acp::method::AgentMethod::SessionNew,
))
}
#[cfg(test)]
mod initialize_request_round_trip {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn initialize_request_round_trips(
request in arbitrary_initialize_request()
) {
let json = serde_json::to_string(&request)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
let decoded: InitializeRequest = serde_json::from_str(&json)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
prop_assert_eq!(request, decoded);
}
#[test]
fn initialize_response_round_trips(
response in arbitrary_initialize_response()
) {
let json = serde_json::to_string(&response)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
let decoded: InitializeResponse = serde_json::from_str(&json)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
prop_assert_eq!(response, decoded);
}
#[test]
fn initialize_request_uses_camel_case_on_the_wire(
request in arbitrary_initialize_request()
) {
let value: serde_json::Value = serde_json::to_value(&request)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
let object = value.as_object()
.ok_or_else(|| TestCaseError::Fail("not an object".into()))?;
prop_assert!(object.contains_key("protocolVersion"));
prop_assert!(!object.contains_key("protocol_version"));
}
#[test]
fn initialize_request_omits_client_info_when_none(
request in arbitrary_initialize_request()
) {
let value: serde_json::Value = serde_json::to_value(&request)
.map_err(|error| TestCaseError::Fail(error.to_string().into()))?;
let object = value.as_object()
.ok_or_else(|| TestCaseError::Fail("not an object".into()))?;
if request.client_info.is_none() {
prop_assert!(!object.contains_key("clientInfo"));
} else {
prop_assert!(object.contains_key("clientInfo"));
}
}
}
}
#[cfg(test)]
mod initialize_request_schema_conformance {
use jsonschema::Validator;
use super::*;
fn full_validator() -> Result<Validator, String> {
let schema_value: serde_json::Value = serde_json::from_str(ACP_SCHEMA_JSON)
.map_err(|error| format!("the vendored schema is not valid JSON: {error}"))?;
jsonschema::validator_for(&schema_value)
.map_err(|error| format!("the vendored schema is invalid: {error}"))
}
fn envelope_for(request: &InitializeRequest) -> Result<serde_json::Value, String> {
let params = serde_json::to_value(request)
.map_err(|error| format!("serialization failed: {error}"))?;
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": params,
}))
}
#[test]
fn initialize_request_validates_against_the_vendored_schema() -> Result<(), String> {
let validator = full_validator()?;
let request = InitializeRequest {
protocol_version: AcpProtocolVersion::CURRENT,
client_capabilities: ClientCapabilities::default(),
client_info: Some(Implementation::protocol_droid(
"protocol-droid-test",
"1.2.3",
)),
};
let envelope = envelope_for(&request)?;
let result = validator.validate(&envelope);
if let Err(error) = result {
return Err(format!(
"the typed InitializeRequest does not validate at `/{}`: {}",
error.instance_path(),
error
));
}
Ok(())
}
#[test]
fn a_minimal_initialize_request_validates_against_the_schema() -> Result<(), String> {
let validator = full_validator()?;
let request = InitializeRequest {
protocol_version: AcpProtocolVersion::CURRENT,
client_capabilities: ClientCapabilities::default(),
client_info: None,
};
let envelope = envelope_for(&request)?;
let result = validator.validate(&envelope);
if let Err(error) = result {
return Err(format!(
"the minimal typed InitializeRequest does not validate at `/{}`: {}",
error.instance_path(),
error
));
}
Ok(())
}
}
#[test]
fn acp_method_session_new_is_dot_separated() {
let method = AcpMethod::Agent(protocol_droid::domain::acp::method::AgentMethod::SessionNew);
assert_eq!(method.as_wire_name(), "session/new");
}
#[cfg(test)]
mod acp_method_wire_name {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(8))]
#[test]
fn acp_method_wire_name_is_session_new(
method in arbitrary_acp_method()
) {
prop_assert_eq!(method.as_wire_name(), "session/new");
}
}
}