use std::process::Command;
fn boundary_cmd() -> Command {
Command::new(env!("CARGO_BIN_EXE_boundary"))
}
fn fixture(name: &str) -> String {
format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))
}
fn analyze_json(fixture_name: &str) -> serde_json::Value {
let path = fixture(fixture_name);
let output = boundary_cmd()
.args(["analyze", &path, "--format", "json"])
.output()
.unwrap_or_else(|e| panic!("failed to run boundary analyze on {fixture_name}: {e}"));
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"boundary analyze failed on {fixture_name}: stdout={stdout}, stderr={stderr}"
);
serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("invalid JSON from {fixture_name}: {e}\noutput: {stdout}"))
}
fn analyze_text(fixture_name: &str) -> String {
let path = fixture(fixture_name);
let output = boundary_cmd()
.args(["analyze", &path])
.output()
.unwrap_or_else(|e| panic!("failed to run boundary analyze (text) on {fixture_name}: {e}"));
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"boundary analyze (text) failed on {fixture_name}: stdout={stdout}, stderr={stderr}"
);
stdout.to_string()
}
fn find_pattern<'a>(json: &'a serde_json::Value, name: &str) -> Option<&'a serde_json::Value> {
json["pattern_detection"]["patterns"]
.as_array()?
.iter()
.find(|entry| entry["name"].as_str() == Some(name))
}
fn pattern_confidence(json: &serde_json::Value, name: &str) -> f64 {
let entry = find_pattern(json, name).unwrap_or_else(|| {
panic!("pattern '{name}' not found in pattern_detection.patterns: {json}")
});
entry["confidence"]
.as_f64()
.unwrap_or_else(|| panic!("confidence for '{name}' is not a number: {entry}"))
}
#[test]
fn pattern_detection_json_shape_contract() {
let json = analyze_json("pattern-ddd-project");
let pd = json
.get("pattern_detection")
.unwrap_or_else(|| panic!("'pattern_detection' object missing from JSON: {json}"));
let patterns = pd["patterns"]
.as_array()
.unwrap_or_else(|| panic!("'pattern_detection.patterns' is not an array: {pd}"));
let expected_names = [
"ddd-hexagonal",
"active-record",
"flat-crud",
"anemic-domain",
"service-layer",
];
for name in &expected_names {
let entry = patterns
.iter()
.find(|e| e["name"].as_str() == Some(name))
.unwrap_or_else(|| {
panic!("pattern '{name}' missing from patterns array: {patterns:?}")
});
assert!(
entry.get("confidence").is_some(),
"pattern '{name}' is missing 'confidence' field: {entry}"
);
let conf = entry["confidence"]
.as_f64()
.unwrap_or_else(|| panic!("confidence for '{name}' is not a number: {entry}"));
assert!(
(0.0..=1.0).contains(&conf),
"confidence for '{name}' is out of [0.0, 1.0]: {conf}"
);
}
assert!(
pd.get("top_pattern").and_then(|v| v.as_str()).is_some(),
"'top_pattern' should be a string field in pattern_detection: {pd}"
);
assert!(
pd.get("top_confidence").and_then(|v| v.as_f64()).is_some(),
"'top_confidence' should be a number field in pattern_detection: {pd}"
);
}
#[test]
fn pattern_ddd_hexagonal_is_top_pattern() {
let json = analyze_json("pattern-ddd-project");
let top = json["pattern_detection"]["top_pattern"]
.as_str()
.unwrap_or_else(|| panic!("'top_pattern' missing or not a string: {json}"));
assert_eq!(
top, "ddd-hexagonal",
"expected top_pattern to be 'ddd-hexagonal', got '{top}'"
);
}
#[test]
fn pattern_ddd_hexagonal_confidence_at_least_half() {
let json = analyze_json("pattern-ddd-project");
let conf = pattern_confidence(&json, "ddd-hexagonal");
assert!(
conf >= 0.5,
"expected ddd-hexagonal confidence >= 0.5, got {conf}"
);
}
#[test]
fn pattern_score_included_when_high_confidence() {
let json = analyze_json("pattern-ddd-project");
let score = json
.get("score")
.unwrap_or_else(|| panic!("'score' object missing when top confidence >= 0.5: {json}"));
assert!(
score.get("overall").and_then(|v| v.as_f64()).is_some(),
"'score.overall' missing or not a number: {score}"
);
assert!(
score
.get("layer_conformance")
.and_then(|v| v.as_f64())
.is_some(),
"'score.layer_conformance' missing or not a number: {score}"
);
assert!(
score
.get("dependency_compliance")
.and_then(|v| v.as_f64())
.is_some(),
"'score.dependency_compliance' missing or not a number: {score}"
);
}
#[test]
fn pattern_text_output_shows_name_and_confidence() {
let text = analyze_text("pattern-ddd-project");
assert!(
text.contains("ddd-hexagonal") || text.to_lowercase().contains("hexagonal"),
"text output should include the detected pattern name: {text}"
);
let has_decimal = text.chars().any(|c| c == '.')
&& text
.split_whitespace()
.any(|w| w.trim_end_matches('%').parse::<f64>().is_ok());
assert!(
has_decimal,
"text output should include the top confidence as a decimal or percentage: {text}"
);
}
#[test]
fn pattern_flat_crud_confidence_at_least_half() {
let json = analyze_json("pattern-flat-crud");
let conf = pattern_confidence(&json, "flat-crud");
assert!(
conf >= 0.5,
"expected flat-crud confidence >= 0.5 for single-package all-concrete project, got {conf}"
);
}
#[test]
fn pattern_anemic_domain_confidence_at_least_half() {
let json = analyze_json("pattern-anemic-domain");
let conf = pattern_confidence(&json, "anemic-domain");
assert!(
conf >= 0.5,
"expected anemic-domain confidence >= 0.5 for all-concrete domain with logic in services, got {conf}"
);
}
#[test]
fn pattern_score_omitted_when_low_confidence() {
let json = analyze_json("pattern-low-confidence");
assert!(
json.get("pattern_detection").is_some(),
"'pattern_detection' object should still be present: {json}"
);
let top_conf = json["pattern_detection"]["top_confidence"]
.as_f64()
.unwrap_or_else(|| panic!("'top_confidence' missing or not a number: {json}"));
assert!(
top_conf < 0.5,
"expected top_confidence < 0.5 for structurally neutral fixture, got {top_conf}"
);
assert!(
json.get("score").is_none(),
"'score' object should be absent when top confidence < 0.5: {json}"
);
}
#[test]
fn pattern_text_no_overall_score_when_low_confidence() {
let text = analyze_text("pattern-low-confidence");
assert!(
!text.contains("Overall Score"),
"text output should not include 'Overall Score' when no pattern is dominant: {text}"
);
}
#[test]
fn pattern_transition_multiple_nonzero_confidences() {
let json = analyze_json("pattern-transition");
let patterns = json["pattern_detection"]["patterns"]
.as_array()
.unwrap_or_else(|| panic!("'pattern_detection.patterns' missing or not an array: {json}"));
let nonzero_count = patterns
.iter()
.filter(|entry| entry["confidence"].as_f64().unwrap_or(0.0) > 0.0)
.count();
assert!(
nonzero_count > 1,
"expected more than one pattern with confidence > 0.0 for a transition project, got {nonzero_count}: {patterns:?}"
);
}