use std::path::PathBuf;
use terraphim_automata::{ThesaurusBuilder, builder::Logseq};
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_kg_error(err: &str) -> bool {
err.contains("No such file or directory")
|| err.contains("KG path does not exist")
|| err.contains("Failed to build thesaurus")
|| err.contains("Knowledge graph not configured")
|| err.contains("not found")
|| err.contains("thesaurus")
|| err.contains("automata")
|| err.contains("IO error")
|| err.contains("Io error")
}
fn extract_clean_output(output: &str) -> String {
output
.lines()
.filter(|line| {
!line.contains("INFO")
&& !line.contains("WARN")
&& !line.contains("DEBUG")
&& !line.contains("OpenDal")
&& !line.contains("Creating role")
&& !line.contains("Successfully built thesaurus")
&& !line.contains("Starting summarization worker")
&& !line.contains("Failed to load config")
&& !line.contains("Failed to load thesaurus")
&& !line.contains("ERROR")
&& !line.trim().is_empty()
})
.collect::<Vec<&str>>()
.join("\n")
}
async fn build_test_thesaurus() -> Result<terraphim_types::Thesaurus, Box<dyn std::error::Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let manifest_path = PathBuf::from(manifest_dir);
let workspace_root = manifest_path
.parent()
.and_then(|p| p.parent())
.ok_or("Cannot find workspace root from CARGO_MANIFEST_DIR")?;
let kg_path = workspace_root.join("docs/src/kg");
if !kg_path.exists() {
return Err(format!(
"KG path does not exist: {:?}\nworkspace_root: {:?}\nmanifest_dir: {:?}",
kg_path, workspace_root, manifest_path
)
.into());
}
let logseq_builder = Logseq::default();
let thesaurus = logseq_builder
.build("test_role".to_string(), kg_path)
.await?;
Ok(thesaurus)
}
async fn replace_with_kg(
text: &str,
link_type: terraphim_automata::LinkType,
) -> Result<String, Box<dyn std::error::Error>> {
let thesaurus = build_test_thesaurus().await?;
let result = terraphim_automata::replace_matches(text, thesaurus, link_type)?;
Ok(String::from_utf8(result)?)
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[tokio::test]
async fn test_replace_npm_to_bun() {
let result = replace_with_kg("npm", terraphim_automata::LinkType::PlainText).await;
match result {
Ok(output) => {
assert!(
output.contains("bun"),
"Expected 'bun' in output, got: {}",
output
);
}
Err(e) => {
let err_str = e.to_string();
if is_ci_environment() && is_ci_expected_kg_error(&err_str) {
println!(
"Test skipped in CI - KG fixtures unavailable: {}",
err_str.lines().next().unwrap_or("")
);
return;
}
panic!("Failed to perform replacement: {}", e);
}
}
}
#[tokio::test]
async fn test_replace_yarn_to_bun() {
let result = replace_with_kg("yarn", terraphim_automata::LinkType::PlainText).await;
match result {
Ok(output) => {
assert!(
output.contains("bun"),
"Expected 'bun' in output, got: {}",
output
);
}
Err(e) => {
let err_str = e.to_string();
if is_ci_environment() && is_ci_expected_kg_error(&err_str) {
println!(
"Test skipped in CI - KG fixtures unavailable: {}",
err_str.lines().next().unwrap_or("")
);
return;
}
panic!("Failed to perform replacement: {}", e);
}
}
}
#[tokio::test]
async fn test_replace_pnpm_install_to_bun() {
let result = replace_with_kg("pnpm install", terraphim_automata::LinkType::PlainText).await;
match result {
Ok(output) => {
assert!(
output.contains("bun install"),
"Expected 'bun install' in output, got: {}",
output
);
}
Err(e) => {
let err_str = e.to_string();
if is_ci_environment() && is_ci_expected_kg_error(&err_str) {
println!(
"Test skipped in CI - KG fixtures unavailable: {}",
err_str.lines().next().unwrap_or("")
);
return;
}
panic!("Failed to perform replacement: {}", e);
}
}
}
#[tokio::test]
async fn test_replace_yarn_install_to_bun() {
let result = replace_with_kg("yarn install", terraphim_automata::LinkType::PlainText).await;
match result {
Ok(output) => {
assert!(
output.contains("bun install"),
"Expected 'bun install' in output, got: {}",
output
);
}
Err(e) => {
let err_str = e.to_string();
if is_ci_environment() && is_ci_expected_kg_error(&err_str) {
println!(
"Test skipped in CI - KG fixtures unavailable: {}",
err_str.lines().next().unwrap_or("")
);
return;
}
panic!("Failed to perform replacement: {}", e);
}
}
}
#[tokio::test]
async fn test_replace_with_markdown_format() {
let result = replace_with_kg("npm", terraphim_automata::LinkType::MarkdownLinks).await;
match result {
Ok(output) => {
assert!(
output.contains("[bun]"),
"Expected '[bun]' in markdown output, got: {}",
output
);
}
Err(e) => {
let err_str = e.to_string();
if is_ci_environment() && is_ci_expected_kg_error(&err_str) {
println!(
"Test skipped in CI - KG fixtures unavailable: {}",
err_str.lines().next().unwrap_or("")
);
return;
}
panic!("Failed to perform replacement: {}", e);
}
}
}
#[test]
fn test_replace_help_output() {
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"-p",
"terraphim_agent",
"--bin",
"terraphim-agent",
"--",
"replace",
"--help",
])
.output()
.expect("Failed to execute command");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("replace") || stdout.contains("Replace"),
"Help output should mention replace command"
);
assert!(
stdout.contains("text") || stdout.contains("TEXT"),
"Help output should mention text argument"
);
}
#[test]
fn test_extract_clean_output_helper() {
let raw_output = r#"INFO: Starting process
DEBUG: Loading configuration
bun
WARN: Some warning
ERROR: Failed to load thesaurus
"#;
let clean = extract_clean_output(raw_output);
assert_eq!(clean, "bun");
}
#[test]
fn test_extract_clean_output_multiline() {
let raw_output = r#"[2025-10-06T19:35:46Z WARN opendal::services] service=memory
bun install
[2025-10-06T19:35:46Z ERROR terraphim_service] Failed to load
"#;
let clean = extract_clean_output(raw_output);
assert_eq!(clean, "bun install");
}
#[tokio::test]
async fn test_url_protection_plain_url() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("example"),
NormalizedTerm::new(1u64, NormalizedTermValue::from("example"))
.with_display_value("REPLACED".to_string()),
);
let text = "Visit https://example.com for more info";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("https://example.com"),
"URL should be protected, got: {}",
result_str
);
}
#[tokio::test]
async fn test_url_protection_markdown_link() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("claude"),
NormalizedTerm::new(1u64, NormalizedTermValue::from("claude"))
.with_display_value("Terraphim".to_string()),
);
let text = "[Claude](https://claude.ai/code)";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("https://claude.ai/code"),
"Markdown link URL should be protected, got: {}",
result_str
);
assert!(
result_str.contains("Terraphim"),
"Display text should be replaced, got: {}",
result_str
);
}
#[tokio::test]
async fn test_url_protection_email() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("anthropic"),
NormalizedTerm::new(1u64, NormalizedTermValue::from("anthropic"))
.with_display_value("Company".to_string()),
);
let text = "Contact noreply@anthropic.com for help";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("noreply@anthropic.com"),
"Email should be protected, got: {}",
result_str
);
}
#[tokio::test]
async fn test_case_preservation_with_display_value() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("claude code"), NormalizedTerm::new(1u64, NormalizedTermValue::from("terraphim ai"))
.with_display_value("Terraphim AI".to_string()),
);
let text = "Using Claude Code for development";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("Terraphim AI"),
"Should preserve case from display_value, got: {}",
result_str
);
assert!(
!result_str.contains("terraphim ai"),
"Should not output lowercase, got: {}",
result_str
);
}
#[tokio::test]
async fn test_fallback_when_no_display_value() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("foo"),
NormalizedTerm::new(1u64, NormalizedTermValue::from("bar")), );
let text = "Replace foo here";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("bar"),
"Should fallback to normalized value, got: {}",
result_str
);
}
#[tokio::test]
async fn test_issue_394_combined_scenario() {
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
let mut thesaurus = Thesaurus::new("test".to_string());
thesaurus.insert(
NormalizedTermValue::from("claude code"),
NormalizedTerm::new(1u64, NormalizedTermValue::from("terraphim ai"))
.with_display_value("Terraphim AI".to_string()),
);
let text = "Generated with [Claude Code](https://claude.ai/claude-code)";
let result = terraphim_automata::replace_matches(
text,
thesaurus,
terraphim_automata::LinkType::PlainText,
)
.expect("Replacement should succeed");
let result_str = String::from_utf8(result).expect("Valid UTF-8");
assert!(
result_str.contains("Terraphim AI"),
"Display text should use proper case, got: {}",
result_str
);
assert!(
result_str.contains("https://claude.ai/claude-code"),
"URL should be protected, got: {}",
result_str
);
}
}