use std::fs;
use tempfile::TempDir;
#[cfg(test)]
fn pmat_bin_path() -> Option<std::path::PathBuf> {
let test_exe = std::env::current_exe().expect("current_exe");
let target_debug = test_exe.parent().unwrap().parent().unwrap();
let debug_bin = target_debug.join("pmat");
if debug_bin.exists() {
return Some(debug_bin);
}
let target_dir = target_debug.parent().unwrap();
let release_bin = target_dir.join("release").join("pmat");
if release_bin.exists() {
return Some(release_bin);
}
None
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod red_phase_tests {
use super::*;
#[tokio::test]
async fn red_must_show_individual_function_names() {
let Some(bin) = pmat_bin_path() else {
eprintln!("pmat binary not found, skipping integration test");
return;
};
let temp_dir = TempDir::new().unwrap();
let ts_content = r#"
function calculateTotal() { return 42; }
function processData() { return "data"; }
const validateInput = () => { return true; };
"#;
fs::write(temp_dir.path().join("test.ts"), ts_content).unwrap();
let output = std::process::Command::new(bin)
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("calculateTotal"),
"Missing function name 'calculateTotal' in output"
);
assert!(
stdout.contains("processData"),
"Missing function name 'processData' in output"
);
assert!(
stdout.contains("validateInput"),
"Missing function name 'validateInput' in output"
);
}
#[tokio::test]
async fn red_must_show_file_level_breakdown() {
let Some(bin) = pmat_bin_path() else {
eprintln!("pmat binary not found, skipping integration test");
return;
};
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("auth.ts"),
"function login() {} function logout() {}",
)
.unwrap();
fs::write(
temp_dir.path().join("utils.ts"),
"function formatDate() {} function parseJSON() {}",
)
.unwrap();
let output = std::process::Command::new(bin)
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("File: auth.ts") || stdout.contains("auth.ts"),
"Missing file-level grouping for auth.ts"
);
assert!(
stdout.contains("File: utils.ts") || stdout.contains("utils.ts"),
"Missing file-level grouping for utils.ts"
);
let auth_index = stdout.find("auth.ts").unwrap_or(0);
let utils_index = stdout.find("utils.ts").unwrap_or(0);
let login_index = stdout.find("login").unwrap_or(0);
let format_index = stdout.find("formatDate").unwrap_or(0);
assert!(
login_index > auth_index && login_index < utils_index,
"Function 'login' not properly grouped under auth.ts"
);
assert!(
format_index > utils_index,
"Function 'formatDate' not properly grouped under utils.ts"
);
}
#[tokio::test]
#[ignore = "Requires pmat binary to be built"]
async fn red_must_show_complexity_scores() {
let temp_dir = TempDir::new().unwrap();
let complex_function = COMPLEX_JS_FUNCTION;
fs::write(temp_dir.path().join("complex.js"), complex_function).unwrap();
let output = std::process::Command::new(pmat_bin_path().unwrap())
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert_complexity_output(&stdout);
}
const COMPLEX_JS_FUNCTION: &str = r#"
function complexLogic(input) {
if (input > 10) {
if (input > 20) {
for (let i = 0; i < input; i++) {
if (i % 2 === 0) {
console.log(i);
}
}
}
} else {
switch(input) {
case 1: return "one";
case 2: return "two";
default: return "other";
}
}
}
"#;
fn assert_complexity_output(stdout: &str) {
assert!(
stdout.contains("complexity")
|| stdout.contains("Complexity")
|| stdout.contains("cyclomatic"),
"Missing complexity metrics in output"
);
assert!(
stdout.contains("complexLogic")
&& (stdout.contains("high") || stdout.contains("High") || stdout.contains("⚠")),
"Missing high complexity warning for complex function"
);
}
#[tokio::test]
#[ignore = "Requires pmat binary to be built"]
async fn red_must_show_satd_annotations() {
let temp_dir = TempDir::new().unwrap();
let code_with_debt = r#"
// TODO: Refactor this to use async/await
function oldStyleCallback(cb) {
setTimeout(() => {
cb("done");
}, 1000);
}
// FIXME: This has a memory leak
function leakyFunction() {
// HACK: Using global to store state
window.globalState = window.globalState || [];
window.globalState.push(new Array(1000000));
}
"#;
fs::write(temp_dir.path().join("debt.js"), code_with_debt).unwrap();
let output = std::process::Command::new(pmat_bin_path().unwrap())
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("TODO") || stdout.contains("Technical Debt") || stdout.contains("SATD"),
"Missing SATD annotations"
);
assert!(
stdout.contains("FIXME") || stdout.contains("memory leak"),
"Missing FIXME annotation for memory leak"
);
assert!(
stdout.contains("HACK") || stdout.contains("global state"),
"Missing HACK annotation"
);
}
#[tokio::test]
#[ignore = "Requires pmat binary to be built"]
async fn red_must_show_quality_insights() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("long.js"),
format!(
"function veryLong() {{\n{}}}",
"console.log('line');\n".repeat(200) + "}"
),
)
.unwrap();
fs::write(temp_dir.path().join("duplicate.js"),
"function copy1() { return 42; }\nfunction copy2() { return 42; }\nfunction copy3() { return 42; }").unwrap();
let output = std::process::Command::new(pmat_bin_path().unwrap())
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Quality")
|| stdout.contains("Insights")
|| stdout.contains("Recommendations"),
"Missing quality insights section"
);
assert!(
stdout.contains("long")
|| stdout.contains("Long")
|| stdout.contains("lines")
|| stdout.contains("LOC"),
"Missing insight about long function"
);
}
#[tokio::test]
async fn red_must_show_dead_code_markers() {
let Some(bin) = pmat_bin_path() else {
eprintln!("pmat binary not found, skipping integration test");
return;
};
let temp_dir = TempDir::new().unwrap();
let code_with_dead = r#"
function usedFunction() {
return "I am used";
}
function unusedFunction() {
return "I am never called";
}
// Export shows what's actually used
export { usedFunction };
"#;
fs::write(temp_dir.path().join("mixed.js"), code_with_dead).unwrap();
let output = std::process::Command::new(bin)
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("unusedFunction")
&& (stdout.contains("dead")
|| stdout.contains("Dead")
|| stdout.contains("unused")
|| stdout.contains("⚠")),
"Missing dead code marker for unused function"
);
}
#[tokio::test]
#[ignore = "Requires pmat binary to be built"]
async fn red_must_show_wasm_function_details() {
let temp_dir = TempDir::new().unwrap();
let wasm_content = r#"
(module
(func $fibonacci (param $n i32) (result i32)
local.get $n
i32.const 2
i32.lt_s
if (result i32)
local.get $n
else
local.get $n
i32.const 1
i32.sub
call $fibonacci
local.get $n
i32.const 2
i32.sub
call $fibonacci
i32.add
end
)
(export "fibonacci" (func $fibonacci))
)
"#;
fs::write(temp_dir.path().join("math.wat"), wasm_content).unwrap();
let output = std::process::Command::new(pmat_bin_path().unwrap())
.args([
"context",
"--project-path",
temp_dir.path().to_str().unwrap(),
"--format",
"llm-optimized",
])
.output()
.expect("Failed to run pmat");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("fibonacci") || stdout.contains("$fibonacci"),
"Missing WASM function name"
);
assert!(
stdout.contains("WASM") || stdout.contains("WebAssembly") || stdout.contains(".wat"),
"Missing WASM type annotation"
);
assert!(
stdout.contains("export") || stdout.contains("Export"),
"Missing export annotation for WASM function"
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod green_phase_implementation {
pub(super) fn format_context_with_annotations(
analysis_report: &crate::services::simple_deep_context::SimpleAnalysisReport,
project_path: &std::path::Path,
) -> String {
let mut output = String::new();
output.push_str(&format!(
"Project: {} (detected)\n\n",
project_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
));
output.push_str("Summary:\n");
output.push_str(&format!("- Files: {}\n", analysis_report.file_count));
output.push_str(&format!(
"- Functions: {}\n",
analysis_report.complexity_metrics.total_functions
));
output.push('\n');
output.push_str("Key Components:\n\n");
for file_detail in &analysis_report.file_complexity_details {
output.push_str(&format!("File: {}\n", file_detail.file_path.display()));
output.push_str(&format!(" Functions: {}\n", file_detail.function_count));
if file_detail.high_complexity_functions > 0 {
output.push_str(&format!(
" ⚠ High Complexity: {} functions\n",
file_detail.high_complexity_functions
));
}
output.push_str(&format!(
" Average Complexity: {:.1}\n",
file_detail.avg_complexity
));
output.push('\n');
}
if analysis_report.complexity_metrics.high_complexity_count > 0 {
output.push_str("Quality Insights:\n");
output.push_str(&format!(
"- {} functions have high complexity and should be refactored\n",
analysis_report.complexity_metrics.high_complexity_count
));
output.push_str(&format!(
"- Average complexity: {:.1}\n",
analysis_report.complexity_metrics.avg_complexity
));
output.push('\n');
}
if !analysis_report.recommendations.is_empty() {
output.push_str("Recommendations:\n");
for rec in &analysis_report.recommendations {
output.push_str(&format!("- {}\n", rec));
}
}
output
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod refactor_phase_quality {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn property_annotations_preserve_function_count(file_count in 1u8..=50, functions_per_file in 1u8..=10) {
let total_functions = file_count as usize * functions_per_file as usize;
let mut file_details = Vec::new();
for i in 0..file_count {
file_details.push(crate::services::simple_deep_context::FileComplexityDetail {
file_path: format!("file{}.js", i).into(),
function_count: functions_per_file as usize,
high_complexity_functions: 0,
avg_complexity: 1.0,
complexity_score: 1.0,
function_names: vec![format!("function{}", i)],
});
}
let report = crate::services::simple_deep_context::SimpleAnalysisReport {
file_count: file_count as usize,
analysis_duration: std::time::Duration::from_secs(1),
complexity_metrics: crate::services::simple_deep_context::ComplexityMetrics {
total_functions,
high_complexity_count: 0,
avg_complexity: 1.0,
},
recommendations: vec![],
file_complexity_details: file_details,
};
let output = green_phase_implementation::format_context_with_annotations(
&report,
std::path::Path::new("test"),
);
let expected = format!("Functions: {}", total_functions);
prop_assert!(output.contains(&expected));
}
#[test]
fn property_all_files_appear_in_output(file_names in proptest::collection::vec("[a-zA-Z][a-zA-Z0-9]{0,10}", 1..10)) {
let mut file_details = Vec::new();
for name in &file_names {
file_details.push(crate::services::simple_deep_context::FileComplexityDetail {
file_path: format!("{}.js", name).into(),
function_count: 1,
high_complexity_functions: 0,
avg_complexity: 1.0,
complexity_score: 1.0,
function_names: vec![format!("function_{}", name)],
});
}
let report = crate::services::simple_deep_context::SimpleAnalysisReport {
file_count: file_names.len(),
analysis_duration: std::time::Duration::from_secs(1),
complexity_metrics: crate::services::simple_deep_context::ComplexityMetrics {
total_functions: file_names.len(),
high_complexity_count: 0,
avg_complexity: 1.0,
},
recommendations: vec![],
file_complexity_details: file_details,
};
let output = green_phase_implementation::format_context_with_annotations(
&report,
std::path::Path::new("test"),
);
for name in &file_names {
let expected = format!("{}.js", name);
prop_assert!(output.contains(&expected));
}
}
#[test]
fn property_high_complexity_triggers_warning(high_complexity_count in 0u8..=10) {
let has_high_complexity = high_complexity_count > 0;
let report = crate::services::simple_deep_context::SimpleAnalysisReport {
file_count: 1,
analysis_duration: std::time::Duration::from_secs(1),
complexity_metrics: crate::services::simple_deep_context::ComplexityMetrics {
total_functions: 10,
high_complexity_count: high_complexity_count as usize,
avg_complexity: if has_high_complexity { 15.0 } else { 3.0 },
},
recommendations: if has_high_complexity {
vec!["Refactor high complexity functions".to_string()]
} else {
vec![]
},
file_complexity_details: vec![
crate::services::simple_deep_context::FileComplexityDetail {
file_path: "test.js".into(),
function_count: 10,
high_complexity_functions: high_complexity_count as usize,
avg_complexity: if has_high_complexity { 15.0 } else { 3.0 },
complexity_score: if has_high_complexity { 15.0 } else { 3.0 },
function_names: vec!["testFunction".to_string()],
},
],
};
let output = green_phase_implementation::format_context_with_annotations(
&report,
std::path::Path::new("test"),
);
if has_high_complexity {
prop_assert!(
output.contains("⚠")
|| output.contains("High Complexity")
|| output.contains("high complexity"),
);
} else {
prop_assert!(!output.contains("⚠"));
}
}
}
}