Skip to content

Commit d7c4c20

Browse files
authored
test: add unit tests to zero-coverage and low-coverage crates (#635)
Adds unit tests to zero/low-coverage crates: haystack_core, terraphim_hooks, terraphim_onepassword_cli, terraphim_sessions. ~1000 lines of new test code.
1 parent 1b70483 commit d7c4c20

8 files changed

Lines changed: 1009 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/haystack_core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ repository = "https://github.com/terraphim/terraphim-ai"
99

1010
[dependencies]
1111
terraphim_types = { path = "../terraphim_types", version = "1.0.0" }
12+
13+
[dev-dependencies]
14+
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

crates/haystack_core/src/lib.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,140 @@ pub trait HaystackProvider {
66
#[allow(async_fn_in_trait)]
77
async fn search(&self, query: &SearchQuery) -> Result<Vec<Document>, Self::Error>;
88
}
9+
10+
#[cfg(test)]
11+
mod tests {
12+
use super::*;
13+
use terraphim_types::NormalizedTermValue;
14+
15+
/// A concrete test provider that returns pre-configured documents.
16+
struct TestProvider {
17+
documents: Vec<Document>,
18+
}
19+
20+
impl TestProvider {
21+
fn with_docs(documents: Vec<Document>) -> Self {
22+
Self { documents }
23+
}
24+
25+
fn empty() -> Self {
26+
Self {
27+
documents: Vec::new(),
28+
}
29+
}
30+
}
31+
32+
/// Error type for the test provider.
33+
#[derive(Debug)]
34+
struct TestProviderError(String);
35+
36+
impl std::fmt::Display for TestProviderError {
37+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38+
write!(f, "TestProviderError: {}", self.0)
39+
}
40+
}
41+
42+
impl HaystackProvider for TestProvider {
43+
type Error = TestProviderError;
44+
45+
async fn search(&self, _query: &SearchQuery) -> Result<Vec<Document>, Self::Error> {
46+
Ok(self.documents.clone())
47+
}
48+
}
49+
50+
/// A provider that always returns an error.
51+
struct FailingProvider;
52+
53+
impl HaystackProvider for FailingProvider {
54+
type Error = TestProviderError;
55+
56+
async fn search(&self, _query: &SearchQuery) -> Result<Vec<Document>, Self::Error> {
57+
Err(TestProviderError("search failed".to_string()))
58+
}
59+
}
60+
61+
fn make_query(term: &str) -> SearchQuery {
62+
SearchQuery {
63+
search_term: NormalizedTermValue::from(term),
64+
..Default::default()
65+
}
66+
}
67+
68+
fn make_document(id: &str, title: &str) -> Document {
69+
Document {
70+
id: id.to_string(),
71+
title: title.to_string(),
72+
..Default::default()
73+
}
74+
}
75+
76+
#[tokio::test]
77+
async fn test_provider_returns_documents() {
78+
let provider = TestProvider::with_docs(vec![
79+
make_document("1", "First Result"),
80+
make_document("2", "Second Result"),
81+
]);
82+
let results = provider.search(&make_query("test")).await.unwrap();
83+
assert_eq!(results.len(), 2);
84+
assert_eq!(results[0].title, "First Result");
85+
assert_eq!(results[1].title, "Second Result");
86+
}
87+
88+
#[tokio::test]
89+
async fn test_provider_returns_empty_results() {
90+
let provider = TestProvider::empty();
91+
let results = provider.search(&make_query("nothing")).await.unwrap();
92+
assert!(results.is_empty());
93+
}
94+
95+
#[tokio::test]
96+
async fn test_provider_error_propagation() {
97+
let provider = FailingProvider;
98+
let result = provider.search(&make_query("test")).await;
99+
assert!(result.is_err());
100+
let err = result.unwrap_err();
101+
assert!(err.to_string().contains("search failed"));
102+
}
103+
104+
#[tokio::test]
105+
async fn test_error_type_is_send_sync() {
106+
fn assert_send_sync<T: Send + Sync + 'static>() {}
107+
assert_send_sync::<TestProviderError>();
108+
}
109+
110+
#[tokio::test]
111+
async fn test_provider_with_empty_search_term() {
112+
let provider = TestProvider::with_docs(vec![make_document("1", "Doc")]);
113+
let results = provider.search(&make_query("")).await.unwrap();
114+
assert_eq!(results.len(), 1);
115+
}
116+
117+
#[tokio::test]
118+
async fn test_provider_with_special_characters_in_query() {
119+
let provider = TestProvider::with_docs(vec![make_document("1", "Doc")]);
120+
let results = provider
121+
.search(&make_query("test & <script>alert(1)</script>"))
122+
.await
123+
.unwrap();
124+
assert_eq!(results.len(), 1);
125+
}
126+
127+
#[tokio::test]
128+
async fn test_concurrent_searches() {
129+
let provider =
130+
std::sync::Arc::new(TestProvider::with_docs(vec![make_document("1", "Result")]));
131+
132+
let mut handles = Vec::new();
133+
for _ in 0..10 {
134+
let p = provider.clone();
135+
handles.push(tokio::spawn(async move {
136+
p.search(&make_query("concurrent")).await.unwrap()
137+
}));
138+
}
139+
140+
for handle in handles {
141+
let results = handle.await.unwrap();
142+
assert_eq!(results.len(), 1);
143+
}
144+
}
145+
}

crates/terraphim_hooks/src/replacement.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,115 @@ mod tests {
189189
assert_eq!(result.result, "original");
190190
assert_eq!(result.error, Some("error msg".to_string()));
191191
}
192+
193+
#[test]
194+
fn test_replacement_multiple_terms_in_one_text() {
195+
let thesaurus = create_test_thesaurus();
196+
let service = ReplacementService::new(thesaurus);
197+
198+
// Both npm and yarn should be replaced by bun
199+
let result = service.replace("npm install && yarn add foo").unwrap();
200+
assert!(result.changed);
201+
assert_eq!(result.result, "bun install && bun add foo");
202+
}
203+
204+
#[test]
205+
fn test_replacement_service_pnpm() {
206+
let thesaurus = create_test_thesaurus();
207+
let service = ReplacementService::new(thesaurus);
208+
209+
let result = service.replace("pnpm install express").unwrap();
210+
assert!(result.changed);
211+
assert_eq!(result.result, "bun install express");
212+
}
213+
214+
#[test]
215+
fn test_find_matches_returns_matched_terms() {
216+
let thesaurus = create_test_thesaurus();
217+
let service = ReplacementService::new(thesaurus);
218+
219+
let matches = service.find_matches("npm install && yarn add").unwrap();
220+
assert!(!matches.is_empty());
221+
let match_terms: Vec<&str> = matches.iter().map(|m| m.term.as_str()).collect();
222+
assert!(match_terms.contains(&"npm"));
223+
assert!(match_terms.contains(&"yarn"));
224+
}
225+
226+
#[test]
227+
fn test_contains_matches_true() {
228+
let thesaurus = create_test_thesaurus();
229+
let service = ReplacementService::new(thesaurus);
230+
assert!(service.contains_matches("npm install"));
231+
}
232+
233+
#[test]
234+
fn test_contains_matches_false() {
235+
let thesaurus = create_test_thesaurus();
236+
let service = ReplacementService::new(thesaurus);
237+
assert!(!service.contains_matches("cargo build"));
238+
}
239+
240+
#[test]
241+
fn test_replace_fail_open_on_valid_input() {
242+
let thesaurus = create_test_thesaurus();
243+
let service = ReplacementService::new(thesaurus);
244+
245+
let result = service.replace_fail_open("npm install");
246+
assert!(result.changed);
247+
assert_eq!(result.result, "bun install");
248+
assert!(result.error.is_none());
249+
}
250+
251+
#[test]
252+
fn test_hook_result_success_when_unchanged() {
253+
let result = HookResult::success("same".to_string(), "same".to_string());
254+
assert!(!result.changed);
255+
assert_eq!(result.replacements, 0);
256+
}
257+
258+
#[test]
259+
fn test_hook_result_serde_round_trip() {
260+
let result = HookResult::success("npm".to_string(), "bun".to_string());
261+
let json = serde_json::to_string(&result).unwrap();
262+
let deserialized: HookResult = serde_json::from_str(&json).unwrap();
263+
assert_eq!(deserialized.result, "bun");
264+
assert_eq!(deserialized.original, "npm");
265+
assert!(deserialized.changed);
266+
}
267+
268+
#[test]
269+
fn test_hook_result_fail_open_serialization_skips_none_error() {
270+
let result = HookResult::pass_through("text".to_string());
271+
let json = serde_json::to_string(&result).unwrap();
272+
assert!(!json.contains("error"));
273+
}
274+
275+
#[test]
276+
fn test_replacement_with_unicode_text() {
277+
let thesaurus = create_test_thesaurus();
278+
let service = ReplacementService::new(thesaurus);
279+
280+
let result = service.replace("npm install -- emoji").unwrap();
281+
assert!(result.changed);
282+
assert!(result.result.starts_with("bun"));
283+
}
284+
285+
#[test]
286+
fn test_replacement_preserves_surrounding_text() {
287+
let thesaurus = create_test_thesaurus();
288+
let service = ReplacementService::new(thesaurus);
289+
290+
let result = service.replace("before npm after").unwrap();
291+
assert!(result.changed);
292+
assert_eq!(result.result, "before bun after");
293+
}
294+
295+
#[test]
296+
fn test_with_link_type_builder_pattern() {
297+
let thesaurus = create_test_thesaurus();
298+
let service = ReplacementService::new(thesaurus).with_link_type(LinkType::PlainText);
299+
// Just verify it compiles and doesn't panic
300+
let result = service.replace("npm install").unwrap();
301+
assert!(result.changed);
302+
}
192303
}

0 commit comments

Comments
 (0)