use crate::helpers::{metric_to_string, parse_metric, parse_storage_mode, storage_mode_to_string};
use crate::types::{
default_metric, default_top_k, default_vector_weight, BatchSearchRequest, CollectionInfo,
CreateCollectionRequest, DeletePointsRequest, GetPointsRequest, HybridResult,
HybridSearchRequest, PointOutput, QueryRequest, QueryResponse, SearchRequest, SearchResponse,
SearchResult, TextSearchRequest,
};
use toml;
#[test]
fn test_parse_metric_cosine() {
let result = parse_metric("cosine");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_euclidean() {
let result = parse_metric("euclidean");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_l2_alias() {
let result = parse_metric("l2");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_dot() {
let result = parse_metric("dot");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_hamming() {
let result = parse_metric("hamming");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_jaccard() {
let result = parse_metric("jaccard");
assert!(result.is_ok());
}
#[test]
fn test_parse_metric_invalid() {
let result = parse_metric("unknown");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Unknown metric"));
}
#[test]
fn test_parse_metric_case_insensitive() {
assert!(parse_metric("COSINE").is_ok());
assert!(parse_metric("Euclidean").is_ok());
assert!(parse_metric("DOT").is_ok());
}
#[test]
fn test_metric_to_string() {
use velesdb_core::distance::DistanceMetric;
assert_eq!(metric_to_string(DistanceMetric::Cosine), "cosine");
assert_eq!(metric_to_string(DistanceMetric::Euclidean), "euclidean");
assert_eq!(metric_to_string(DistanceMetric::DotProduct), "dot");
assert_eq!(metric_to_string(DistanceMetric::Hamming), "hamming");
assert_eq!(metric_to_string(DistanceMetric::Jaccard), "jaccard");
}
#[test]
fn test_default_metric() {
let metric = default_metric();
assert_eq!(metric, "cosine");
}
#[test]
fn test_default_top_k() {
let k = default_top_k();
assert_eq!(k, 10);
}
#[test]
fn test_default_vector_weight() {
let weight = default_vector_weight();
assert!((weight - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_create_collection_request_deserialize() {
let json = r#"{"name": "test", "dimension": 768}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.name, "test");
assert_eq!(request.dimension, 768);
assert_eq!(request.metric, "cosine");
assert_eq!(request.storage_mode, "full");
assert!(request.hnsw_m.is_none());
assert!(request.hnsw_ef_construction.is_none());
assert!(request.hnsw_alpha.is_none());
assert!(request.hnsw_max_elements.is_none());
assert!(request.pq_rescore_oversampling.is_none());
}
#[test]
fn test_create_collection_request_with_all_hnsw_params() {
let json = r#"{
"name": "custom",
"dimension": 384,
"metric": "euclidean",
"storageMode": "sq8",
"hnswM": 48,
"hnswEfConstruction": 600,
"hnswAlpha": 1.5,
"hnswMaxElements": 500000,
"pqRescoreOversampling": 8
}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.name, "custom");
assert_eq!(request.dimension, 384);
assert_eq!(request.metric, "euclidean");
assert_eq!(request.storage_mode, "sq8");
assert_eq!(request.hnsw_m, Some(48));
assert_eq!(request.hnsw_ef_construction, Some(600));
assert!((request.hnsw_alpha.unwrap() - 1.5).abs() < f32::EPSILON);
assert_eq!(request.hnsw_max_elements, Some(500_000));
assert_eq!(request.pq_rescore_oversampling, Some(8));
}
#[test]
fn test_create_collection_request_with_partial_hnsw_params() {
let json = r#"{"name": "partial", "dimension": 128, "hnswM": 16}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.hnsw_m, Some(16));
assert!(request.hnsw_ef_construction.is_none());
assert!(request.hnsw_alpha.is_none());
assert!(request.hnsw_max_elements.is_none());
assert!(request.pq_rescore_oversampling.is_none());
}
#[test]
fn test_build_hnsw_params_uses_auto_defaults() {
use crate::commands::build_hnsw_params;
let json = r#"{"name": "test", "dimension": 768}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
let params = build_hnsw_params(&request, velesdb_core::StorageMode::Full);
let auto = velesdb_core::HnswParams::auto(768);
assert_eq!(params.max_connections, auto.max_connections);
assert_eq!(params.ef_construction, auto.ef_construction);
assert_eq!(params.max_elements, auto.max_elements);
assert!((params.alpha - auto.alpha).abs() < f32::EPSILON);
}
#[test]
fn test_build_hnsw_params_overrides_selectively() {
use crate::commands::build_hnsw_params;
let json = r#"{"name": "test", "dimension": 128, "hnswM": 64, "hnswAlpha": 1.0}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
let params = build_hnsw_params(&request, velesdb_core::StorageMode::SQ8);
let auto = velesdb_core::HnswParams::auto(128);
assert_eq!(params.max_connections, 64);
assert_eq!(params.ef_construction, auto.ef_construction);
assert_eq!(params.max_elements, auto.max_elements);
assert!((params.alpha - 1.0).abs() < f32::EPSILON);
assert_eq!(params.storage_mode, velesdb_core::StorageMode::SQ8);
}
#[test]
fn test_has_advanced_params_false_when_all_none() {
use crate::commands::has_advanced_params;
let json = r#"{"name": "test", "dimension": 768}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert!(!has_advanced_params(&request));
}
#[test]
fn test_has_advanced_params_true_when_any_set() {
use crate::commands::has_advanced_params;
let json = r#"{"name": "test", "dimension": 768, "pqRescoreOversampling": 4}"#;
let request: CreateCollectionRequest = serde_json::from_str(json).unwrap();
assert!(has_advanced_params(&request));
}
#[test]
fn test_search_request_deserialize() {
let json = r#"{"collection": "docs", "vector": [0.1, 0.2, 0.3]}"#;
let request: SearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.vector, vec![0.1, 0.2, 0.3]);
assert_eq!(request.top_k, 10);
assert!(request.quality.is_none(), "quality should default to None");
}
#[test]
fn test_search_request_with_quality() {
let json = r#"{"collection": "docs", "vector": [0.1, 0.2], "quality": "fast"}"#;
let request: SearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.quality, Some("fast".to_string()));
}
#[test]
fn test_search_request_with_quality_camel_case() {
let json = r#"{"collection": "docs", "vector": [0.1], "topK": 5, "quality": "accurate"}"#;
let request: SearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.top_k, 5);
assert_eq!(request.quality, Some("accurate".to_string()));
}
#[test]
fn test_individual_search_request_with_quality() {
use crate::types::IndividualSearchRequest;
let json = r#"{"vector": [0.1, 0.2], "quality": "balanced"}"#;
let request: IndividualSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.quality, Some("balanced".to_string()));
}
#[test]
fn test_individual_search_request_quality_defaults_none() {
use crate::types::IndividualSearchRequest;
let json = r#"{"vector": [0.1, 0.2]}"#;
let request: IndividualSearchRequest = serde_json::from_str(json).unwrap();
assert!(request.quality.is_none());
}
#[test]
fn test_collection_info_serialize() {
let info = CollectionInfo {
name: "test".to_string(),
dimension: 768,
metric: "cosine".to_string(),
count: 100,
storage_mode: "full".to_string(),
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"dimension\":768"));
assert!(json.contains("\"metric\":\"cosine\""));
assert!(json.contains("\"count\":100"));
assert!(json.contains("\"storageMode\":\"full\""));
}
#[test]
fn test_search_result_serialize() {
let result = SearchResult {
id: 42,
score: 0.95,
payload: Some(serde_json::json!({"title": "Test"})),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"id\":\"42\""));
assert!(json.contains("\"score\":0.95"));
assert!(json.contains("\"title\":\"Test\""));
}
#[test]
fn test_get_points_request_deserialize() {
let json = r#"{"collection": "docs", "ids": [1, 2, 3]}"#;
let request: GetPointsRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.ids, vec![1, 2, 3]);
}
#[test]
fn test_delete_points_request_deserialize() {
let json = r#"{"collection": "docs", "ids": [1, 2]}"#;
let request: DeletePointsRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.ids, vec![1, 2]);
}
#[test]
fn test_batch_search_request_deserialize() {
let json = r#"{"collection": "docs", "searches": [{"vector": [0.1, 0.2]}, {"vector": [0.3, 0.4], "topK": 5}]}"#;
let request: BatchSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.searches.len(), 2);
assert_eq!(request.searches[0].vector, vec![0.1, 0.2]);
assert_eq!(request.searches[0].top_k, 10);
assert_eq!(request.searches[1].vector, vec![0.3, 0.4]);
assert_eq!(request.searches[1].top_k, 5);
}
#[test]
fn test_point_output_serialize() {
let point = PointOutput {
id: 1,
vector: vec![0.1, 0.2, 0.3],
payload: Some(serde_json::json!({"key": "value"})),
};
let json = serde_json::to_string(&point).unwrap();
assert!(json.contains("\"id\":1"));
assert!(json.contains("\"vector\":[0.1,0.2,0.3]"));
assert!(json.contains("\"key\":\"value\""));
}
#[test]
fn test_text_search_request_deserialize() {
let json = r#"{"collection": "docs", "query": "machine learning"}"#;
let request: TextSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.query, "machine learning");
assert_eq!(request.top_k, 10);
}
#[test]
fn test_hybrid_search_request_deserialize() {
let json = r#"{"collection": "docs", "vector": [0.1, 0.2], "query": "test"}"#;
let request: HybridSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.collection, "docs");
assert_eq!(request.vector, vec![0.1, 0.2]);
assert_eq!(request.query, "test");
assert_eq!(request.top_k, 10);
assert!((request.vector_weight - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_parse_storage_mode_full() {
let result = parse_storage_mode("full");
assert!(result.is_ok());
}
#[test]
fn test_parse_storage_mode_sq8() {
let result = parse_storage_mode("sq8");
assert!(result.is_ok());
}
#[test]
fn test_parse_storage_mode_binary() {
let result = parse_storage_mode("binary");
assert!(result.is_ok());
}
#[test]
fn test_parse_storage_mode_invalid() {
let result = parse_storage_mode("unknown");
assert!(result.is_err());
}
#[test]
fn test_storage_mode_to_string() {
use velesdb_core::StorageMode;
assert_eq!(storage_mode_to_string(StorageMode::Full), "full");
assert_eq!(storage_mode_to_string(StorageMode::SQ8), "sq8");
assert_eq!(storage_mode_to_string(StorageMode::Binary), "binary");
assert_eq!(
storage_mode_to_string(StorageMode::ProductQuantization),
"pq"
);
}
#[test]
fn test_search_response_serialize() {
let response = SearchResponse {
results: vec![SearchResult {
id: 1,
score: 0.9,
payload: None,
}],
timing_ms: 1.5,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"results\""));
assert!(json.contains("\"timingMs\":1.5"));
}
#[test]
fn test_query_request_deserialize_simple() {
let json = r#"{"query": "MATCH (d:Doc) RETURN d"}"#;
let request: QueryRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.query, "MATCH (d:Doc) RETURN d");
assert!(request.params.is_empty());
}
#[test]
fn test_query_request_deserialize_with_params() {
let json = r#"{
"query": "MATCH (d:Doc) WHERE similarity(d.embedding, $q) > 0.7 RETURN d",
"params": {"q": [0.1, 0.2, 0.3]}
}"#;
let request: QueryRequest = serde_json::from_str(json).unwrap();
assert!(request.query.contains("similarity"));
assert!(request.params.contains_key("q"));
}
#[test]
fn test_query_request_camel_case() {
let json = r#"{"query": "SELECT * FROM docs"}"#;
let request: QueryRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.query, "SELECT * FROM docs");
}
#[test]
fn test_hybrid_result_serialize() {
let result = HybridResult {
node_id: 42,
vector_score: Some(0.95),
graph_score: Some(0.88),
fused_score: 0.92,
bindings: Some(serde_json::json!({"title": "Test Document"})),
column_data: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"nodeId\":42"));
assert!(json.contains("\"vectorScore\":0.95"));
assert!(json.contains("\"graphScore\":0.88"));
assert!(json.contains("\"fusedScore\":0.92"));
assert!(json.contains("\"title\":\"Test Document\""));
}
#[test]
fn test_hybrid_result_serialize_minimal() {
let result = HybridResult {
node_id: 1,
vector_score: None,
graph_score: None,
fused_score: 0.5,
bindings: None,
column_data: None,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"nodeId\":1"));
assert!(json.contains("\"fusedScore\":0.5"));
assert!(json.contains("\"vectorScore\":null"));
}
#[test]
fn test_hybrid_result_with_column_data() {
let result = HybridResult {
node_id: 10,
vector_score: Some(0.9),
graph_score: None,
fused_score: 0.9,
bindings: None,
column_data: Some(serde_json::json!({"price": 99.99, "currency": "USD"})),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"columnData\""));
assert!(json.contains("\"price\":99.99"));
assert!(json.contains("\"currency\":\"USD\""));
}
#[test]
fn test_query_response_serialize() {
let response = QueryResponse {
results: vec![
HybridResult {
node_id: 1,
vector_score: Some(0.95),
graph_score: None,
fused_score: 0.95,
bindings: Some(serde_json::json!({"name": "Doc1"})),
column_data: None,
},
HybridResult {
node_id: 2,
vector_score: Some(0.85),
graph_score: None,
fused_score: 0.85,
bindings: Some(serde_json::json!({"name": "Doc2"})),
column_data: None,
},
],
timing_ms: 2.5,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"results\""));
assert!(json.contains("\"timingMs\":2.5"));
assert!(json.contains("\"nodeId\":1"));
assert!(json.contains("\"nodeId\":2"));
}
#[test]
fn test_query_response_empty_results() {
let response = QueryResponse {
results: vec![],
timing_ms: 0.1,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"results\":[]"));
assert!(json.contains("\"timingMs\":0.1"));
}
const REGISTERED_COMMANDS: &[&str] = &[
"create_collection",
"create_metadata_collection",
"delete_collection",
"list_collections",
"get_collection",
"is_empty",
"flush",
"scroll_collection",
"upsert",
"upsert_metadata",
"get_points",
"delete_points",
"search",
"batch_search",
"text_search",
"hybrid_search",
"multi_query_search",
"query",
"semantic_store",
"semantic_query",
"episodic_record",
"episodic_recent",
"procedural_learn",
"procedural_recall",
"create_graph_collection",
"add_edge",
"get_edges",
"traverse_graph",
"get_node_degree",
"traverse_graph_parallel",
"sparse_search",
"hybrid_sparse_search",
"sparse_upsert",
"train_pq",
"stream_insert",
"create_index",
"drop_index",
"list_indexes",
];
#[test]
fn test_all_commands_have_default_permissions_toml_parsed() {
let default_toml_content = include_str!("../permissions/default.toml");
let parsed: toml::Value =
toml::from_str(default_toml_content).expect("Failed to parse default.toml as valid TOML");
let default_section = parsed
.get("default")
.expect("Missing [default] section in default.toml");
let permissions = default_section
.get("permissions")
.expect("Missing 'permissions' array in [default] section")
.as_array()
.expect("'permissions' should be an array");
let permission_strings: Vec<&str> = permissions.iter().filter_map(|v| v.as_str()).collect();
for cmd in REGISTERED_COMMANDS {
let expected_permission = format!("allow-{}", cmd.replace('_', "-"));
assert!(
permission_strings.contains(&expected_permission.as_str()),
"Missing permission '{expected_permission}' in [default] section for command '{cmd}'.\n\
Add '\"{expected_permission}\"' to the [default] permissions array in default.toml.\n\
Note: The permission must be in the [default] section, not just anywhere in the file."
);
}
}
#[test]
fn test_default_permissions_count_matches_commands() {
let default_toml_content = include_str!("../permissions/default.toml");
let parsed: toml::Value =
toml::from_str(default_toml_content).expect("Failed to parse default.toml as valid TOML");
let default_section = parsed
.get("default")
.expect("Missing [default] section in default.toml");
let permissions = default_section
.get("permissions")
.expect("Missing 'permissions' array in [default] section")
.as_array()
.expect("'permissions' should be an array");
assert_eq!(
permissions.len(),
REGISTERED_COMMANDS.len(),
"Mismatch between number of permissions ({}) and registered commands ({}).\n\
This may indicate orphaned permissions or missing command registrations.",
permissions.len(),
REGISTERED_COMMANDS.len()
);
}
#[test]
fn test_all_commands_have_default_permissions() {
let default_toml = include_str!("../permissions/default.toml");
for cmd in REGISTERED_COMMANDS {
let permission = format!("allow-{}", cmd.replace('_', "-"));
assert!(
default_toml.contains(&permission),
"Missing permission '{permission}' in default.toml for command '{cmd}'.\n\
Add '\"{permission}\"' to the [default] permissions array."
);
}
}
#[test]
fn test_delete_points_permission_exists() {
let default_toml = include_str!("../permissions/default.toml");
assert!(
default_toml.contains("allow-delete-points"),
"Regression: 'allow-delete-points' permission missing from default.toml (Issue #169)"
);
}
#[test]
fn test_build_rs_commands_match_registered() {
let build_rs_content = include_str!("../build.rs");
for cmd in REGISTERED_COMMANDS {
assert!(
build_rs_content.contains(&format!("\"{cmd}\"")),
"Command '{cmd}' is registered but missing from build.rs COMMANDS array.\n\
Add '\"{cmd}\"' to the COMMANDS array in build.rs to generate its permission file."
);
}
}
#[test]
fn test_scroll_request_deserialize() {
use crate::types::ScrollRequest;
let json = r#"{"collection": "docs"}"#;
let req: ScrollRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.collection, "docs");
assert!(req.cursor.is_none());
assert_eq!(req.batch_size, 100); assert!(req.filter.is_none());
}
#[test]
fn test_scroll_request_with_cursor() {
use crate::types::ScrollRequest;
let json = r#"{"collection": "docs", "cursor": 42, "batchSize": 50}"#;
let req: ScrollRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.cursor, Some(42));
assert_eq!(req.batch_size, 50);
}
#[test]
fn test_scroll_response_serialize() {
use crate::types::{PointOutput, ScrollResponse};
let resp = ScrollResponse {
points: vec![PointOutput {
id: 1,
vector: vec![0.1, 0.2],
payload: None,
}],
next_cursor: Some(2),
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"nextCursor\":2"));
assert!(json.contains("\"id\":1"));
}
#[test]
fn test_episodic_record_request_deserialize() {
use crate::types::EpisodicRecordRequest;
let json = r#"{"eventId": 42, "content": "found key", "timestamp": 1700000000, "embedding": [0.1, 0.2]}"#;
let req: EpisodicRecordRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.event_id, 42);
assert_eq!(req.content, "found key");
assert_eq!(req.timestamp, 1_700_000_000);
assert_eq!(req.embedding.len(), 2);
}
#[test]
fn test_episodic_recent_request_defaults() {
use crate::types::EpisodicRecentRequest;
let json = r"{}";
let req: EpisodicRecentRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.limit, 10); assert!(req.since_timestamp.is_none());
}
#[test]
fn test_episodic_recent_request_with_since_timestamp() {
use crate::types::EpisodicRecentRequest;
let json = r#"{"limit": 5, "sinceTimestamp": 1700000000}"#;
let req: EpisodicRecentRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.limit, 5);
assert_eq!(req.since_timestamp, Some(1_700_000_000));
}
#[test]
fn test_procedural_learn_request_deserialize() {
use crate::types::ProceduralLearnRequest;
let json = r#"{"procedureId": 1, "name": "login", "steps": ["open app", "enter creds"], "embedding": [0.5], "confidence": 0.9}"#;
let req: ProceduralLearnRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.procedure_id, 1);
assert_eq!(req.name, "login");
assert_eq!(req.steps.len(), 2);
assert!((req.confidence - 0.9).abs() < f32::EPSILON);
}
#[test]
fn test_procedural_learn_request_default_confidence() {
use crate::types::ProceduralLearnRequest;
let json = r#"{"procedureId": 1, "name": "test", "steps": [], "embedding": [0.1]}"#;
let req: ProceduralLearnRequest = serde_json::from_str(json).unwrap();
assert!((req.confidence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_procedural_recall_request_deserialize() {
use crate::types::ProceduralRecallRequest;
let json = r#"{"embedding": [0.1, 0.2], "topK": 5, "minConfidence": 0.5}"#;
let req: ProceduralRecallRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.top_k, 5);
assert!((req.min_confidence - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_procedural_recall_request_defaults() {
use crate::types::ProceduralRecallRequest;
let json = r#"{"embedding": [0.1]}"#;
let req: ProceduralRecallRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.top_k, 10); assert!((req.min_confidence - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_episodic_result_serialize() {
use crate::types::EpisodicResult;
let result = EpisodicResult {
id: 42,
content: "found key".to_string(),
timestamp: 1_700_000_000,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"id\":42"));
assert!(json.contains("\"content\":\"found key\""));
assert!(json.contains("\"timestamp\":1700000000"));
}
#[test]
fn test_procedural_match_result_serialize() {
use crate::types::ProceduralMatchResult;
let result = ProceduralMatchResult {
id: 1,
name: "login".to_string(),
steps: vec!["open app".to_string(), "enter creds".to_string()],
confidence: 0.9,
score: 0.85,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"id\":1"));
assert!(json.contains("\"name\":\"login\""));
assert!(json.contains("\"steps\""));
assert!(json.contains("\"confidence\":0.9"));
assert!(json.contains("\"score\":0.85"));
}
#[test]
fn test_scroll_response_no_next_cursor() {
use crate::types::ScrollResponse;
let resp = ScrollResponse {
points: vec![],
next_cursor: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(!json.contains("nextCursor"));
}