#[allow(deprecated)] use assert_cmd::Command;
#[allow(unused_imports)] use predicates::prelude::*;
use serial_test::serial;
#[allow(deprecated)] fn cli_command() -> Command {
Command::cargo_bin("terraphim-cli").unwrap()
}
fn run_cli_json(args: &[&str]) -> Result<serde_json::Value, String> {
let output = Command::cargo_bin("terraphim-cli")
.map_err(|e| format!("Failed to find binary: {}", e))?
.args(args)
.output()
.map_err(|e| format!("Failed to execute: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Err(format!(
"Command failed with exit code {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
));
}
serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse JSON: {} - output: {}", e, stdout))
}
#[cfg(test)]
mod role_switching_tests {
use super::*;
#[test]
#[serial]
fn test_list_roles() {
let result = run_cli_json(&["roles"]);
match result {
Ok(json) => {
assert!(json.is_array(), "Roles should be an array");
let roles = json.as_array().unwrap();
assert!(!roles.is_empty(), "Should have at least one role");
}
Err(e) => {
eprintln!("Roles test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_config_shows_selected_role() {
let result = run_cli_json(&["config"]);
match result {
Ok(json) => {
assert!(
json.get("selected_role").is_some(),
"Config should have selected_role"
);
let selected = json["selected_role"].as_str().unwrap();
assert!(!selected.is_empty(), "Selected role should not be empty");
}
Err(e) => {
eprintln!("Config test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_with_default_role() {
let result = run_cli_json(&["search", "test query"]);
match result {
Ok(json) => {
assert!(json.get("role").is_some(), "Search result should have role");
let role = json["role"].as_str().unwrap();
assert!(!role.is_empty(), "Role should not be empty");
}
Err(e) => {
eprintln!("Search test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_with_explicit_role() {
let result = run_cli_json(&["search", "test", "--role", "Default"]);
match result {
Ok(json) => {
assert_eq!(
json["role"].as_str(),
Some("Default"),
"Should use specified role"
);
}
Err(e) => {
eprintln!("Search with role test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_graph_with_explicit_role() {
let result = run_cli_json(&["graph", "--role", "Default"]);
match result {
Ok(json) => {
assert_eq!(
json["role"].as_str(),
Some("Default"),
"Should use specified role"
);
}
Err(e) => {
eprintln!("Graph with role test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_find_with_explicit_role() {
let result = run_cli_json(&["find", "test text", "--role", "Default"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Find with role returned error: {:?}", json);
return;
}
assert!(
json.get("text").is_some() || json.get("matches").is_some(),
"Find should have text or matches field"
);
}
Err(e) => {
eprintln!("Find with role test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_with_explicit_role() {
let result = run_cli_json(&["replace", "test text", "--role", "Default"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Replace with role returned error: {:?}", json);
return;
}
assert!(
json.get("original").is_some() || json.get("replaced").is_some(),
"Replace should have original or replaced field: {:?}",
json
);
}
Err(e) => {
eprintln!("Replace with role test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_thesaurus_with_explicit_role() {
let result = run_cli_json(&["thesaurus", "--role", "Default"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Thesaurus with role returned error: {:?}", json);
return;
}
assert!(
json.get("role").is_some()
|| json.get("terms").is_some()
|| json.get("name").is_some(),
"Thesaurus should have role, terms, or name field: {:?}",
json
);
}
Err(e) => {
eprintln!("Thesaurus with role test skipped: {}", e);
}
}
}
}
#[cfg(test)]
mod kg_search_tests {
use super::*;
#[test]
#[serial]
fn test_basic_search() {
let result = run_cli_json(&["search", "rust"]);
match result {
Ok(json) => {
assert_eq!(json["query"].as_str(), Some("rust"));
assert!(json.get("results").is_some());
assert!(json.get("count").is_some());
}
Err(e) => {
eprintln!("Basic search test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_with_limit() {
let result = run_cli_json(&["search", "test", "--limit", "3"]);
match result {
Ok(json) => {
let count = json["count"].as_u64().unwrap_or(0);
assert!(count <= 3, "Results should respect limit");
}
Err(e) => {
eprintln!("Search with limit test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_with_multiple_words() {
let result = run_cli_json(&["search", "rust async programming"]);
match result {
Ok(json) => {
assert_eq!(json["query"].as_str(), Some("rust async programming"));
}
Err(e) => {
eprintln!("Multi-word search test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_returns_array_of_results() {
let result = run_cli_json(&["search", "tokio"]);
match result {
Ok(json) => {
assert!(json["results"].is_array(), "Results should be an array");
}
Err(e) => {
eprintln!("Search results array test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_search_results_have_required_fields() {
let result = run_cli_json(&["search", "api"]);
match result {
Ok(json) => {
if let Some(results) = json["results"].as_array() {
for doc in results {
assert!(doc.get("id").is_some(), "Document should have id");
assert!(doc.get("title").is_some(), "Document should have title");
assert!(doc.get("url").is_some(), "Document should have url");
}
}
}
Err(e) => {
eprintln!("Search results fields test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_graph_returns_concepts() {
let result = run_cli_json(&["graph"]);
match result {
Ok(json) => {
assert!(json.get("concepts").is_some(), "Graph should have concepts");
assert!(json["concepts"].is_array(), "Concepts should be an array");
}
Err(e) => {
eprintln!("Graph concepts test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_graph_with_custom_top_k() {
let result = run_cli_json(&["graph", "--top-k", "5"]);
match result {
Ok(json) => {
assert_eq!(json["top_k"].as_u64(), Some(5));
let concepts = json["concepts"].as_array().unwrap();
assert!(concepts.len() <= 5, "Should return at most 5 concepts");
}
Err(e) => {
eprintln!("Graph top-k test skipped: {}", e);
}
}
}
}
#[cfg(test)]
mod replace_tests {
use super::*;
#[test]
#[serial]
fn test_replace_markdown_format() {
let result = run_cli_json(&["replace", "rust programming", "--link-format", "markdown"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Replace markdown test skipped: Knowledge graph not configured");
return;
}
assert_eq!(json["format"].as_str(), Some("markdown"));
assert_eq!(json["original"].as_str(), Some("rust programming"));
assert!(json.get("replaced").is_some());
}
Err(e) => {
eprintln!("Replace markdown test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_html_format() {
let result = run_cli_json(&["replace", "async tokio", "--link-format", "html"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Replace html test skipped: Knowledge graph not configured");
return;
}
assert_eq!(json["format"].as_str(), Some("html"));
}
Err(e) => {
eprintln!("Replace html test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_wiki_format() {
let result = run_cli_json(&["replace", "docker kubernetes", "--link-format", "wiki"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Replace wiki test skipped: Knowledge graph not configured");
return;
}
assert_eq!(json["format"].as_str(), Some("wiki"));
}
Err(e) => {
eprintln!("Replace wiki test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_plain_format() {
let result = run_cli_json(&["replace", "git github", "--link-format", "plain"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Replace plain test skipped: Knowledge graph not configured");
return;
}
assert_eq!(json["format"].as_str(), Some("plain"));
assert_eq!(
json["original"].as_str(),
json["replaced"].as_str(),
"Plain format should not modify text"
);
}
Err(e) => {
eprintln!("Replace plain test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_default_format_is_markdown() {
let result = run_cli_json(&["replace", "test text"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!(
"Replace default format test skipped: Knowledge graph not configured"
);
return;
}
assert_eq!(
json["format"].as_str(),
Some("markdown"),
"Default format should be markdown"
);
}
Err(e) => {
eprintln!("Replace default format test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_replace_preserves_unmatched_text() {
let result = run_cli_json(&[
"replace",
"some random text without matches xyz123",
"--format",
"markdown",
]);
match result {
Ok(json) => {
let _original = json["original"].as_str().unwrap();
let replaced = json["replaced"].as_str().unwrap();
assert!(replaced.contains("xyz123") || replaced.contains("random"));
}
Err(e) => {
eprintln!("Replace preserves text test skipped: {}", e);
}
}
}
}
#[cfg(test)]
mod find_tests {
use super::*;
#[test]
#[serial]
fn test_find_basic() {
let result = run_cli_json(&["find", "rust async tokio"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Find basic test skipped: Knowledge graph not configured");
return;
}
assert_eq!(json["text"].as_str(), Some("rust async tokio"));
assert!(json.get("matches").is_some());
assert!(json.get("count").is_some());
}
Err(e) => {
eprintln!("Find basic test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_find_returns_array_of_matches() {
let result = run_cli_json(&["find", "api server client"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Find matches array test skipped: Knowledge graph not configured");
return;
}
assert!(json["matches"].is_array(), "Matches should be an array");
}
Err(e) => {
eprintln!("Find matches array test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_find_matches_have_required_fields() {
let result = run_cli_json(&["find", "database json config"]);
match result {
Ok(json) => {
if let Some(matches) = json["matches"].as_array() {
for m in matches {
assert!(m.get("term").is_some(), "Match should have term");
assert!(
m.get("normalized").is_some(),
"Match should have normalized"
);
}
}
}
Err(e) => {
eprintln!("Find matches fields test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_find_count_matches_array_length() {
let result = run_cli_json(&["find", "linux docker kubernetes"]);
match result {
Ok(json) => {
let count = json["count"].as_u64().unwrap_or(0) as usize;
let matches_len = json["matches"].as_array().map(|a| a.len()).unwrap_or(0);
assert_eq!(count, matches_len, "Count should match array length");
}
Err(e) => {
eprintln!("Find count test skipped: {}", e);
}
}
}
}
#[cfg(test)]
mod thesaurus_tests {
use super::*;
#[test]
#[serial]
fn test_thesaurus_basic() {
let result = run_cli_json(&["thesaurus"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Thesaurus basic test skipped: Knowledge graph not configured");
return;
}
assert!(json.get("role").is_some());
assert!(json.get("name").is_some());
assert!(json.get("terms").is_some());
assert!(json.get("total_count").is_some());
assert!(json.get("shown_count").is_some());
}
Err(e) => {
eprintln!("Thesaurus basic test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_thesaurus_with_limit() {
let result = run_cli_json(&["thesaurus", "--limit", "5"]);
match result {
Ok(json) => {
if json.get("error").is_some() {
eprintln!("Thesaurus limit test skipped: Knowledge graph not configured");
return;
}
let shown = json["shown_count"].as_u64().unwrap_or(0);
assert!(shown <= 5, "Should respect limit");
let terms = json["terms"].as_array().unwrap();
assert!(terms.len() <= 5, "Terms array should respect limit");
}
Err(e) => {
eprintln!("Thesaurus limit test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_thesaurus_terms_have_required_fields() {
let result = run_cli_json(&["thesaurus", "--limit", "10"]);
match result {
Ok(json) => {
if let Some(terms) = json["terms"].as_array() {
for term in terms {
assert!(term.get("id").is_some(), "Term should have id");
assert!(term.get("term").is_some(), "Term should have term");
assert!(
term.get("normalized").is_some(),
"Term should have normalized"
);
}
}
}
Err(e) => {
eprintln!("Thesaurus terms fields test skipped: {}", e);
}
}
}
#[test]
#[serial]
fn test_thesaurus_total_count_greater_or_equal_shown() {
let result = run_cli_json(&["thesaurus", "--limit", "5"]);
match result {
Ok(json) => {
let total = json["total_count"].as_u64().unwrap_or(0);
let shown = json["shown_count"].as_u64().unwrap_or(0);
assert!(total >= shown, "Total count should be >= shown count");
}
Err(e) => {
eprintln!("Thesaurus count test skipped: {}", e);
}
}
}
}
#[cfg(test)]
mod output_format_tests {
use super::*;
#[test]
#[serial]
fn test_json_output() {
let output = cli_command()
.args(["--format", "json", "roles"])
.output()
.expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
if !trimmed.is_empty() {
let is_json = (trimmed.starts_with('[') && trimmed.ends_with(']'))
|| (trimmed.starts_with('{') && trimmed.ends_with('}'));
let has_error = trimmed.contains("error") || trimmed.contains("Error");
assert!(
is_json || has_error || output.status.success(),
"Output should be JSON or contain error: {}",
trimmed
);
}
}
#[test]
#[serial]
fn test_json_pretty_output() {
let output = cli_command()
.args(["--format", "json-pretty", "config"])
.output()
.expect("Failed to execute");
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(lines.len() > 1, "Pretty JSON should have multiple lines");
}
}
#[test]
#[serial]
fn test_text_output() {
let output = cli_command()
.args(["--format", "text", "config"])
.output()
.expect("Failed to execute");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!stdout.trim().is_empty() || !output.status.success());
}
}
#[cfg(test)]
mod evaluate_tests {
use super::*;
use tempfile::TempDir;
fn create_temp_ground_truth() -> TempDir {
let dir = TempDir::new().expect("Failed to create temp dir");
let ground_truth_path = dir.path().join("ground_truth.json");
let json = serde_json::json!([
{
"id": "doc1",
"text": "I love rust and async programming",
"expected_terms": [
{"term": "rust", "category": null},
{"term": "async", "category": null}
]
},
{
"id": "doc2",
"text": "Python is great for data science",
"expected_terms": [
{"term": "python", "category": null},
{"term": "data science", "category": null}
]
}
]);
std::fs::write(
&ground_truth_path,
serde_json::to_string_pretty(&json).unwrap(),
)
.unwrap();
dir
}
fn create_temp_thesaurus() -> TempDir {
let dir = TempDir::new().expect("Failed to create temp dir");
let thesaurus_path = dir.path().join("thesaurus.json");
let json = serde_json::json!({
"name": "test",
"data": {
"rust": {"id": 1, "nterm": "rust"},
"async": {"id": 2, "nterm": "async"},
"python": {"id": 3, "nterm": "python"}
}
});
std::fs::write(
&thesaurus_path,
serde_json::to_string_pretty(&json).unwrap(),
)
.unwrap();
dir
}
#[test]
#[serial]
fn test_evaluate_command_success() {
let gt_dir = create_temp_ground_truth();
let th_dir = create_temp_thesaurus();
let gt_path = gt_dir.path().join("ground_truth.json");
let th_path = th_dir.path().join("thesaurus.json");
let result = run_cli_json(&[
"evaluate",
"--ground-truth",
gt_path.to_str().unwrap(),
"--thesaurus",
th_path.to_str().unwrap(),
]);
match result {
Ok(json) => {
assert!(
json.get("total_documents").is_some(),
"Should have total_documents"
);
assert!(json.get("overall").is_some(), "Should have overall metrics");
assert!(json.get("per_term").is_some(), "Should have per_term array");
let overall = &json["overall"];
assert!(
overall.get("precision").is_some(),
"Overall should have precision"
);
assert!(
overall.get("recall").is_some(),
"Overall should have recall"
);
assert!(overall.get("f1").is_some(), "Overall should have f1");
}
Err(e) => {
panic!("Evaluate command failed: {}", e);
}
}
}
#[test]
#[serial]
fn test_evaluate_command_missing_ground_truth() {
let th_dir = create_temp_thesaurus();
let th_path = th_dir.path().join("thesaurus.json");
let output = cli_command()
.args([
"evaluate",
"--ground-truth",
"/nonexistent/path.json",
"--thesaurus",
th_path.to_str().unwrap(),
])
.output()
.expect("Failed to execute");
assert!(
!output.status.success(),
"Should fail with missing ground truth file"
);
}
#[test]
#[serial]
fn test_evaluate_command_missing_thesaurus() {
let gt_dir = create_temp_ground_truth();
let gt_path = gt_dir.path().join("ground_truth.json");
let output = cli_command()
.args([
"evaluate",
"--ground-truth",
gt_path.to_str().unwrap(),
"--thesaurus",
"/nonexistent/path.json",
])
.output()
.expect("Failed to execute");
assert!(
!output.status.success(),
"Should fail with missing thesaurus file"
);
}
#[test]
#[serial]
fn test_evaluate_output_contains_expected_fields() {
let gt_dir = create_temp_ground_truth();
let th_dir = create_temp_thesaurus();
let gt_path = gt_dir.path().join("ground_truth.json");
let th_path = th_dir.path().join("thesaurus.json");
let result = run_cli_json(&[
"evaluate",
"--ground-truth",
gt_path.to_str().unwrap(),
"--thesaurus",
th_path.to_str().unwrap(),
]);
match result {
Ok(json) => {
let overall = &json["overall"];
assert!(overall.get("true_positives").is_some());
assert!(overall.get("false_positives").is_some());
assert!(overall.get("false_negatives").is_some());
assert!(json.get("systematic_errors").is_some());
}
Err(e) => {
panic!("Evaluate command failed: {}", e);
}
}
}
}