use anyhow::Result;
use serial_test::serial;
use std::path::PathBuf;
use std::process::{Child, Command};
use std::str;
use std::sync::Once;
use std::time::Duration;
static SERVER_INIT: Once = Once::new();
static mut SERVER_PORT: u16 = 0;
fn start_test_server() -> Result<(Child, u16)> {
let workspace_root = get_workspace_root();
let port = find_available_port()?;
println!("Starting test server on port {}...", port);
let mut cmd = Command::new("cargo");
cmd.args([
"run",
"-p",
"terraphim_server",
"--",
"--port",
&port.to_string(),
])
.current_dir(&workspace_root)
.env("TERRAPHIM_SERVER_PORT", port.to_string())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let child = cmd.spawn()?;
std::thread::sleep(Duration::from_secs(5));
let client = reqwest::blocking::Client::new();
let health_url = format!("http://127.0.0.1:{}/health", port);
let mut retries = 10;
while retries > 0 {
if let Ok(response) = client
.get(&health_url)
.timeout(Duration::from_secs(2))
.send()
{
if response.status().is_success() {
println!("Test server is ready on port {}", port);
return Ok((child, port));
}
}
std::thread::sleep(Duration::from_secs(1));
retries -= 1;
}
Err(anyhow::anyhow!("Server failed to start on port {}", port))
}
fn find_available_port() -> Result<u16> {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();
drop(listener);
Ok(port)
}
fn ensure_server_running() -> Result<u16> {
unsafe {
SERVER_INIT.call_once(|| {
match start_test_server() {
Ok((child, port)) => {
SERVER_PORT = port;
std::mem::forget(child);
}
Err(e) => {
eprintln!("Failed to start test server: {}", e);
SERVER_PORT = 0;
}
}
});
if SERVER_PORT == 0 {
Err(anyhow::anyhow!("Server failed to start"))
} else {
Ok(SERVER_PORT)
}
}
}
#[allow(dead_code)]
fn is_ci_environment() -> bool {
std::env::var("CI").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
|| (std::env::var("USER").as_deref() == Ok("root")
&& std::path::Path::new("/.dockerenv").exists())
|| std::env::var("HOME").as_deref() == Ok("/root")
}
fn is_ci_expected_error(stderr: &str) -> bool {
stderr.contains("Failed to build thesaurus")
|| stderr.contains("Knowledge graph not configured")
|| stderr.contains("Config error")
|| stderr.contains("Middleware error")
|| stderr.contains("IO error")
|| stderr.contains("Builder error")
|| stderr.contains("thesaurus")
|| stderr.contains("automata")
|| stderr.contains("Connection refused")
|| stderr.contains("Connection reset")
}
fn get_workspace_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or(manifest_dir)
}
fn run_extract_command_with_port(args: &[&str], port: u16) -> Result<(String, String, i32)> {
let workspace_root = get_workspace_root();
let mut cmd = Command::new("cargo");
cmd.args(["run", "-p", "terraphim_agent", "--", "extract"])
.args(args)
.current_dir(&workspace_root)
.env(
"TERRAPHIM_API_ENDPOINT",
format!("http://127.0.0.1:{}/api", port),
)
.env("TERRAPHIM_SERVER_HOSTNAME", format!("127.0.0.1:{}", port));
let output = cmd.output()?;
Ok((
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
))
}
#[allow(dead_code)]
fn run_extract_command(args: &[&str]) -> Result<(String, String, i32)> {
run_extract_command_with_port(args, 8000)
}
fn extract_clean_output(output: &str) -> String {
output
.lines()
.filter(|line| {
!line.contains("INFO")
&& !line.contains("WARN")
&& !line.contains("DEBUG")
&& !line.starts_with('[') && !line.trim().is_empty()
})
.collect::<Vec<&str>>()
.join("\n")
}
#[test]
#[serial]
fn test_extract_basic_functionality_validation() -> Result<()> {
println!("Validating extract basic functionality");
let port = match ensure_server_running() {
Ok(p) => p,
Err(e) => {
println!("Warning: Could not start test server: {}", e);
println!("Skipping test - server unavailable");
return Ok(());
}
};
let simple_text = "This is a test paragraph.";
let (stdout, stderr, code) = run_extract_command_with_port(&[simple_text], port)?;
if code != 0 {
if is_ci_expected_error(&stderr) {
println!(
"Extract skipped - server/KG issue: {}",
stderr.lines().next().unwrap_or("")
);
return Ok(());
}
panic!(
"Extract should execute successfully: exit_code={}, stderr={}",
code, stderr
);
}
let clean_output = extract_clean_output(&stdout);
if clean_output.contains("No matches found") {
println!("Extract correctly reports no matches for simple text");
assert!(
clean_output.contains("No matches found"),
"Should explicitly state no matches"
);
} else if clean_output.is_empty() {
println!("Extract returns empty result for simple text (no matches)");
} else {
println!("Extract output: {}", clean_output);
println!("Unexpected output for simple text - may have found matches");
}
Ok(())
}
#[test]
#[serial]
fn test_extract_matching_capability() -> Result<()> {
println!("Testing extract matching capability with various inputs");
let port = match ensure_server_running() {
Ok(p) => p,
Err(e) => {
println!("Warning: Could not start test server: {}", e);
println!("Skipping test - server unavailable");
return Ok(());
}
};
let long_content = format!(
"{} {} {}",
"This paragraph discusses system architecture and design patterns.",
"It covers database integration, API services, and configuration management.",
"The framework supports microservice deployment with data processing pipelines."
);
let test_scenarios = vec![
("Empty text", ""),
("Single word", "system"),
(
"Common tech terms",
"system database api service configuration",
),
("Programming terms", "code function class method variable"),
(
"Architecture terms",
"architecture design pattern framework microservice",
),
("Data terms", "data processing pipeline analytics storage"),
(
"Mixed content",
"The system processes data through multiple service layers using configuration files.",
),
("Long content", long_content.as_str()),
];
let mut results = Vec::new();
for (scenario_name, test_text) in &test_scenarios {
println!(" Testing scenario: {}", scenario_name);
let (stdout, stderr, code) = run_extract_command_with_port(&[test_text], port)?;
if code != 0 {
if is_ci_expected_error(&stderr) {
println!(
"Extract skipped - server/KG issue: {}",
stderr.lines().next().unwrap_or("")
);
return Ok(());
}
panic!(
"Extract should succeed for scenario '{}': stderr={}",
scenario_name, stderr
);
}
let clean_output = extract_clean_output(&stdout);
let result = if clean_output.contains("No matches found") {
"no_matches"
} else if clean_output.is_empty() {
"empty"
} else if clean_output.contains("Match") || clean_output.contains("term:") {
"matches_found"
} else {
"unknown_output"
};
results.push((scenario_name, result, clean_output.lines().count()));
match result {
"no_matches" => println!(" No matches found (explicit)"),
"empty" => println!(" Empty output (implicit no matches)"),
"matches_found" => {
println!(
" Matches found! ({} lines)",
clean_output.lines().count()
);
for (i, line) in clean_output.lines().take(3).enumerate() {
println!(
" {}: {}",
i + 1,
line.chars().take(80).collect::<String>()
);
}
}
"unknown_output" => {
println!(" Unknown output format:");
for line in clean_output.lines().take(2) {
println!(" {}", line.chars().take(80).collect::<String>());
}
}
_ => {
println!(" Unexpected result format: {}", result);
}
}
}
println!("\nExtract Matching Capability Analysis:");
let no_matches_count = results
.iter()
.filter(|(_, result, _)| *result == "no_matches")
.count();
let empty_count = results
.iter()
.filter(|(_, result, _)| *result == "empty")
.count();
let matches_count = results
.iter()
.filter(|(_, result, _)| *result == "matches_found")
.count();
let unknown_count = results
.iter()
.filter(|(_, result, _)| *result == "unknown_output")
.count();
println!(" Results summary:");
println!(" Explicit no matches: {}", no_matches_count);
println!(" Empty outputs: {}", empty_count);
println!(" Matches found: {}", matches_count);
println!(" Unknown outputs: {}", unknown_count);
println!(
"EXTRACT EXECUTION IS WORKING: Command executed successfully for all {} scenarios, even if no matches found",
results.len()
);
if matches_count > 0 {
println!("BONUS: Also found matches in {} scenarios", matches_count);
for (scenario_name, result, line_count) in &results {
if *result == "matches_found" {
println!(
" '{}' found matches ({} lines)",
scenario_name, line_count
);
}
}
}
Ok(())
}
#[test]
#[serial]
fn test_extract_with_known_technical_terms() -> Result<()> {
println!("Testing extract with well-known technical terms");
let port = match ensure_server_running() {
Ok(p) => p,
Err(e) => {
println!("Warning: Could not start test server: {}", e);
println!("Skipping test - server unavailable");
return Ok(());
}
};
let known_terms = vec![
"database",
"API",
"service",
"configuration",
"system",
"architecture",
"framework",
"application",
"server",
"client",
];
let mut found_matches = false;
for term in &known_terms {
let test_paragraph = format!(
"This is a paragraph about {} and its implementation. The {} is used in many applications.",
term, term
);
println!(" Testing with term: {}", term);
let (stdout, stderr, code) = run_extract_command_with_port(&[&test_paragraph], port)?;
if code != 0 {
if is_ci_expected_error(&stderr) {
println!(
"Extract skipped - server/KG issue: {}",
stderr.lines().next().unwrap_or("")
);
return Ok(());
}
panic!(
"Extract should succeed for term '{}': stderr={}",
term, stderr
);
}
let clean_output = extract_clean_output(&stdout);
if !clean_output.is_empty() && !clean_output.contains("No matches found") {
found_matches = true;
println!(" Found matches for term: {}", term);
if let Some(first_line) = clean_output.lines().next() {
println!(
" Output: {}",
first_line.chars().take(100).collect::<String>()
);
}
} else {
println!(" No matches for term: {}", term);
}
}
if found_matches {
println!("SUCCESS: Extract functionality is working with known technical terms!");
} else {
println!("INFO: No matches found with known technical terms");
println!(" This suggests either:");
println!(" - No knowledge graph/thesaurus data is available");
println!(" - The terms tested don't exist in the current KG");
println!(" - Extract requires specific text structure or role configuration");
}
Ok(())
}
#[test]
#[serial]
fn test_extract_error_conditions() -> Result<()> {
println!("Testing extract error handling");
let long_text = "a".repeat(100000);
let error_cases = vec![
("Missing argument", vec![]),
(
"Invalid role",
vec!["test text", "--role", "NonExistentRole"],
),
("Invalid flag", vec!["test text", "--invalid-flag"]),
("Very long text", vec![long_text.as_str()]),
];
let workspace_root = get_workspace_root();
for (case_name, args) in error_cases {
println!(" Testing error case: {}", case_name);
let mut cmd = Command::new("cargo");
cmd.args(["run", "-p", "terraphim_agent", "--", "extract"])
.args(&args)
.current_dir(&workspace_root);
let output = cmd.output()?;
let exit_code = output.status.code().unwrap_or(-1);
match case_name {
"Missing argument" | "Invalid flag" => {
assert_ne!(exit_code, 0, "Should fail for case: {}", case_name);
println!(" Correctly failed with exit code: {}", exit_code);
}
"Invalid role" => {
println!(" Handled invalid role with exit code: {}", exit_code);
}
"Very long text" => {
assert!(
exit_code == 0 || exit_code == 1,
"Should handle very long text gracefully, got exit code: {}",
exit_code
);
println!(" Handled very long text with exit code: {}", exit_code);
}
_ => {}
}
}
println!("Error handling validation completed");
Ok(())
}