use nerve_ipc::message::{
AiToken, MAX_RESULTS, MESSAGE_VERSION, MessageError, SearchContext, SearchOptions, SearchQuery,
SearchResult, SearchResultItem, decode_message, decode_search_query, encode_message,
};
fn minimal_context() -> SearchContext {
SearchContext {
url: "https://example.com".into(),
title: "Example".into(),
selection: None,
extract: None,
}
}
fn minimal_query() -> SearchQuery {
SearchQuery {
v: 1,
query: "rust async io".into(),
context: minimal_context(),
opts: SearchOptions::default(),
}
}
#[test]
fn search_query_minimal_roundtrip() {
let q = minimal_query();
let bytes = encode_message(&q).unwrap();
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert_eq!(q, q2);
}
#[test]
fn search_query_full_context_roundtrip() {
let q = SearchQuery {
v: 1,
query: "what is the capital of France?".into(),
context: SearchContext {
url: "https://example.com/page".into(),
title: "Example Page".into(),
selection: Some("highlighted text here".into()),
extract: Some("some extracted body content".into()),
},
opts: SearchOptions {
search: true,
max_results: 20,
},
};
let bytes = encode_message(&q).unwrap();
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert_eq!(q, q2);
}
#[test]
fn search_query_selection_optional_absent() {
let q = SearchQuery {
v: 1,
query: "test".into(),
context: SearchContext {
url: "https://x.com".into(),
title: "X".into(),
selection: None,
extract: Some("extract present".into()),
},
opts: SearchOptions::default(),
};
let bytes = encode_message(&q).unwrap();
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert!(q2.context.selection.is_none());
assert_eq!(q2.context.extract.as_deref(), Some("extract present"));
let json = String::from_utf8(bytes).unwrap();
assert!(!json.contains("\"selection\""));
}
#[test]
fn search_query_extract_optional_absent() {
let q = SearchQuery {
v: 1,
query: "test".into(),
context: SearchContext {
url: "https://x.com".into(),
title: "X".into(),
selection: Some("sel".into()),
extract: None,
},
opts: SearchOptions::default(),
};
let bytes = encode_message(&q).unwrap();
let json = String::from_utf8(bytes.clone()).unwrap();
assert!(!json.contains("\"extract\""));
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert!(q2.context.extract.is_none());
}
#[test]
fn search_query_search_disabled() {
let q = SearchQuery {
v: 1,
query: "local only".into(),
context: minimal_context(),
opts: SearchOptions {
search: false,
max_results: 5,
},
};
let bytes = encode_message(&q).unwrap();
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert!(!q2.opts.search);
}
#[test]
fn search_query_max_results_preserved() {
for mr in [1u32, 10, 50, MAX_RESULTS] {
let q = SearchQuery {
v: 1,
query: "q".into(),
context: minimal_context(),
opts: SearchOptions {
search: true,
max_results: mr,
},
};
let bytes = encode_message(&q).unwrap();
let q2: SearchQuery = decode_message(&bytes).unwrap();
assert_eq!(q2.opts.max_results, mr);
}
}
#[test]
fn search_query_v1_accepted() {
let q = minimal_query();
assert_eq!(q.v, MESSAGE_VERSION);
q.validate().unwrap();
}
#[test]
fn search_query_unsupported_version_rejected() {
let mut q = minimal_query();
q.v = 2;
match q.validate().unwrap_err() {
MessageError::UnsupportedVersion(v) => assert_eq!(v, 2),
e => panic!("expected UnsupportedVersion, got {e:?}"),
}
}
#[test]
fn search_query_version_zero_rejected() {
let mut q = minimal_query();
q.v = 0;
assert!(matches!(
q.validate().unwrap_err(),
MessageError::UnsupportedVersion(0)
));
}
#[test]
fn search_query_empty_query_rejected() {
let mut q = minimal_query();
q.query = String::new();
assert!(matches!(
q.validate().unwrap_err(),
MessageError::EmptyQuery
));
}
#[test]
fn search_query_max_results_zero_rejected() {
let mut q = minimal_query();
q.opts.max_results = 0;
assert!(matches!(
q.validate().unwrap_err(),
MessageError::MaxResultsOutOfRange(0)
));
}
#[test]
fn search_query_max_results_over_limit_rejected() {
let mut q = minimal_query();
q.opts.max_results = MAX_RESULTS + 1;
assert!(matches!(
q.validate().unwrap_err(),
MessageError::MaxResultsOutOfRange(_)
));
}
#[test]
fn decode_search_query_rejects_unknown_version() {
let mut q = minimal_query();
q.v = 99;
let bytes = encode_message(&q).unwrap();
let err = decode_search_query(&bytes).unwrap_err();
assert!(matches!(err, MessageError::UnsupportedVersion(99)));
}
#[test]
fn decode_search_query_validates_on_decode() {
let q = minimal_query();
let bytes = encode_message(&q).unwrap();
let q2 = decode_search_query(&bytes).unwrap();
assert_eq!(q, q2);
}
#[test]
fn search_result_empty_roundtrip() {
let r = SearchResult {
results: vec![],
took_ms: 0,
};
let bytes = encode_message(&r).unwrap();
let r2: SearchResult = decode_message(&bytes).unwrap();
assert_eq!(r, r2);
assert!(r2.results.is_empty());
}
#[test]
fn search_result_one_item_roundtrip() {
let r = SearchResult {
results: vec![SearchResultItem {
url: "https://result.example.com".into(),
title: "A Result".into(),
snippet: "This is a snippet of the result.".into(),
score: 0.95,
}],
took_ms: 12,
};
let bytes = encode_message(&r).unwrap();
let r2: SearchResult = decode_message(&bytes).unwrap();
assert_eq!(r2.results.len(), 1);
assert_eq!(r2.results[0].url, "https://result.example.com");
assert_eq!(r2.took_ms, 12);
}
#[test]
fn search_result_multiple_items_roundtrip() {
let items: Vec<SearchResultItem> = (0..5)
.map(|i| SearchResultItem {
url: format!("https://example.com/{i}"),
title: format!("Result {i}"),
snippet: format!("Snippet {i}"),
score: i as f32 * 0.1,
})
.collect();
let r = SearchResult {
results: items,
took_ms: 99,
};
let bytes = encode_message(&r).unwrap();
let r2: SearchResult = decode_message(&bytes).unwrap();
assert_eq!(r2.results.len(), 5);
assert_eq!(r2.took_ms, 99);
}
#[test]
fn search_result_score_preserved() {
let r = SearchResult {
results: vec![SearchResultItem {
url: "u".into(),
title: "t".into(),
snippet: "s".into(),
score: 0.123_456_8,
}],
took_ms: 1,
};
let bytes = encode_message(&r).unwrap();
let r2: SearchResult = decode_message(&bytes).unwrap();
assert!((r2.results[0].score - 0.123_456_8f32).abs() < 1e-5);
}
#[test]
fn search_result_took_ms_zero() {
let r = SearchResult {
results: vec![],
took_ms: 0,
};
let bytes = encode_message(&r).unwrap();
let r2: SearchResult = decode_message(&bytes).unwrap();
assert_eq!(r2.took_ms, 0);
}
#[test]
fn ai_token_normal_roundtrip() {
let t = AiToken {
t: "Hello, ".into(),
};
let bytes = encode_message(&t).unwrap();
let t2: AiToken = decode_message(&bytes).unwrap();
assert_eq!(t, t2);
}
#[test]
fn ai_token_unicode_roundtrip() {
let t = AiToken {
t: "こんにちは 🦀 世界".into(),
};
let bytes = encode_message(&t).unwrap();
let t2: AiToken = decode_message(&bytes).unwrap();
assert_eq!(t2.t, "こんにちは 🦀 世界");
}
#[test]
fn ai_token_empty_roundtrip() {
let t = AiToken { t: String::new() };
let bytes = encode_message(&t).unwrap();
let t2: AiToken = decode_message(&bytes).unwrap();
assert_eq!(t2.t, "");
}
#[test]
fn cancel_uses_empty_payload() {
use nerve_ipc::codec::encode as frame_encode;
use nerve_ipc::types::{FrameFlags, MessageType, RequestId};
let buf = frame_encode(MessageType::Cancel, FrameFlags::FINAL, RequestId(42), &[]).unwrap();
let frame = nerve_ipc::decode(&buf).unwrap();
assert_eq!(frame.header.msg_type, MessageType::Cancel as u8);
assert!(frame.payload.is_empty());
assert_eq!(frame.header.request_id, 42);
}
#[test]
fn search_query_json_has_v_field() {
let q = minimal_query();
let json = serde_json::to_string(&q).unwrap();
assert!(json.contains("\"v\":1"));
}
#[test]
fn search_query_json_has_required_context_fields() {
let q = minimal_query();
let json = serde_json::to_string(&q).unwrap();
assert!(json.contains("\"url\""));
assert!(json.contains("\"title\""));
}