use super::support::{
build_workspace, empty_cfg_test, four_layer, globset, ports_app_cli_mcp, run_check_d,
};
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use std::collections::HashSet;
fn make_config(adapters: &[&str]) -> CompiledCallParity {
CompiledCallParity {
adapters: adapters.iter().map(|s| s.to_string()).collect(),
target: "application".to_string(),
call_depth: 3,
exclude_targets: globset(&[]),
transparent_wrappers: HashSet::new(),
transparent_macros: HashSet::new(),
promoted_attributes: HashSet::new(),
single_touchpoint: crate::config::architecture::SingleTouchpointMode::default(),
}
}
fn extract_d(findings: &[MatchLocation]) -> Vec<(String, Vec<(String, usize)>)> {
findings
.iter()
.filter_map(|f| match &f.kind {
ViolationKind::CallParityMultiplicityMismatch {
target_fn,
counts_per_adapter,
..
} => Some((target_fn.clone(), counts_per_adapter.clone())),
_ => None,
})
.collect()
}
#[test]
fn check_d_alias_in_one_adapter() {
let ws = build_workspace(&[
("src/application/session.rs", "pub fn search() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
pub fn cmd_grep() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = extract_d(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
let (target, counts) = &pairs[0];
assert!(target.ends_with("session::search"));
let cli_count = counts.iter().find(|(a, _)| a == "cli").map(|(_, c)| *c);
let mcp_count = counts.iter().find(|(a, _)| a == "mcp").map(|(_, c)| *c);
assert_eq!(cli_count, Some(2));
assert_eq!(mcp_count, Some(1));
}
#[test]
fn check_d_balanced_fan_in_no_finding() {
let ws = build_workspace(&[
("src/application/session.rs", "pub fn search() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
pub fn cmd_grep() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
pub fn handle_grep() { search(); }
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
extract_d(&findings).is_empty(),
"balanced fan-in should be silent, got {findings:?}"
);
}
#[test]
fn check_d_three_adapters_one_diverges() {
let ws = build_workspace(&[
("src/application/session.rs", "pub fn search() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
pub fn cmd_grep() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
(
"src/rest/handlers.rs",
r#"
use crate::application::session::search;
pub fn post_search() { search(); }
pub fn post_grep() { search(); }
"#,
),
]);
let cp = make_config(&["cli", "mcp", "rest"]);
let findings = run_check_d(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = extract_d(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
let counts = &pairs[0].1;
let cli_count = counts.iter().find(|(a, _)| a == "cli").map(|(_, c)| *c);
let mcp_count = counts.iter().find(|(a, _)| a == "mcp").map(|(_, c)| *c);
let rest_count = counts.iter().find(|(a, _)| a == "rest").map(|(_, c)| *c);
assert_eq!(cli_count, Some(2));
assert_eq!(mcp_count, Some(1));
assert_eq!(rest_count, Some(2));
}
#[test]
fn check_d_skips_deprecated_alias() {
let ws = build_workspace(&[
("src/application/session.rs", "pub fn search() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
#[deprecated]
pub fn cmd_grep() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
extract_d(&findings).is_empty(),
"deprecated alias should be excluded from D's count, got {findings:?}"
);
}
#[test]
fn check_d_distinct_from_b() {
let ws = build_workspace(&[
("src/application/session.rs", "pub fn search() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
// mcp doesn't touch search at all
pub fn handle_other() {}
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
extract_d(&findings).is_empty(),
"Check D should not fire when target missing entirely from an adapter (that's Check B's job), got {findings:?}"
);
}
#[test]
fn check_d_uses_anchor_as_capability_for_multiplicity() {
let ws = build_workspace(&[
(
"src/ports/handler.rs",
"pub trait Handler { fn handle(&self); }",
),
(
"src/application/logging.rs",
r#"
use crate::ports::handler::Handler;
pub struct LoggingHandler;
impl Handler for LoggingHandler { fn handle(&self) {} }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::ports::handler::Handler;
pub fn cmd_dispatch_a(h: &dyn Handler) { h.handle(); }
pub fn cmd_dispatch_b(h: &dyn Handler) { h.handle(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::ports::handler::Handler;
pub fn mcp_dispatch(h: &dyn Handler) { h.handle(); }
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &ports_app_cli_mcp(), &cp, &empty_cfg_test());
let entries = extract_d(&findings);
let anchor = "crate::ports::handler::Handler::handle";
let anchor_entry = entries
.iter()
.find(|(target, _)| target == anchor)
.unwrap_or_else(|| panic!("Check D missed anchor multiplicity drift, got {entries:?}"));
let counts: std::collections::HashMap<&str, usize> = anchor_entry
.1
.iter()
.map(|(a, c)| (a.as_str(), *c))
.collect();
assert_eq!(counts.get("cli"), Some(&2));
assert_eq!(counts.get("mcp"), Some(&1));
}
#[test]
fn check_d_surfaces_concrete_multiplicity_when_all_adapters_call_direct() {
let ws = build_workspace(&[
(
"src/ports/handler.rs",
"pub trait Handler { fn handle(&self); }",
),
(
"src/application/logging.rs",
r#"
use crate::ports::handler::Handler;
pub struct LoggingHandler;
impl Handler for LoggingHandler { fn handle(&self) {} }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::logging::LoggingHandler;
pub fn cmd_log_a() { LoggingHandler::handle(&LoggingHandler); }
pub fn cmd_log_b() { LoggingHandler::handle(&LoggingHandler); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::logging::LoggingHandler;
pub fn mcp_log() { LoggingHandler::handle(&LoggingHandler); }
"#,
),
]);
let cp = make_config(&["cli", "mcp"]);
let findings = run_check_d(&ws, &ports_app_cli_mcp(), &cp, &empty_cfg_test());
let entries = extract_d(&findings);
let concrete = "crate::application::logging::LoggingHandler::handle";
let concrete_entry = entries
.iter()
.find(|(target, _)| target == concrete)
.unwrap_or_else(|| {
panic!(
"Check D must surface concrete multiplicity drift when all adapters reach via direct concrete; got {entries:?}"
)
});
let counts: std::collections::HashMap<&str, usize> = concrete_entry
.1
.iter()
.map(|(a, c)| (a.as_str(), *c))
.collect();
assert_eq!(
counts.get("cli"),
Some(&2),
"concrete count for cli (two direct UFCS callers); got {counts:?}"
);
assert_eq!(
counts.get("mcp"),
Some(&1),
"concrete count for mcp (one direct UFCS caller); got {counts:?}"
);
}