use super::support::{
build_workspace, empty_cfg_test, four_layer, globset, ports_app_cli_mcp, run_check_b,
};
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use std::collections::HashSet;
fn make_config(
call_depth: usize,
adapters: &[&str],
exclude_targets: &[&str],
) -> CompiledCallParity {
CompiledCallParity {
adapters: adapters.iter().map(|s| s.to_string()).collect(),
target: "application".to_string(),
call_depth,
exclude_targets: globset(exclude_targets),
transparent_wrappers: HashSet::new(),
transparent_macros: HashSet::new(),
promoted_attributes: HashSet::new(),
single_touchpoint: crate::config::architecture::SingleTouchpointMode::default(),
}
}
fn missing_pairs(findings: &[MatchLocation]) -> Vec<(String, Vec<String>)> {
findings
.iter()
.filter_map(|f| match &f.kind {
ViolationKind::CallParityMissingAdapter {
target_fn,
missing_adapters,
..
} => Some((target_fn.clone(), missing_adapters.clone())),
_ => None,
})
.collect()
}
#[test]
fn test_target_fn_called_from_all_adapters_passes() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/rest/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn post_stats() { get_stats(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"covered-by-all should pass, got {findings:?}"
);
}
#[test]
fn test_target_fn_missing_one_adapter_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1);
assert!(pairs[0].0.ends_with("get_stats"));
assert_eq!(pairs[0].1, vec!["rest".to_string()]);
}
#[test]
fn test_target_fn_missing_two_adapters_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1);
let mut missing = pairs[0].1.clone();
missing.sort();
assert_eq!(missing, vec!["mcp".to_string(), "rest".to_string()]);
}
#[test]
fn test_target_fn_not_called_from_any_adapter_lists_all_missing() {
let ws = build_workspace(&[("src/application/stats.rs", "pub fn get_stats() {}")]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1);
let mut missing = pairs[0].1.clone();
missing.sort();
assert_eq!(
missing,
vec!["cli".to_string(), "mcp".to_string(), "rest".to_string()]
);
}
#[test]
fn test_target_fn_reached_transitively_passes() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/rest/service.rs",
r#"
use crate::application::stats::get_stats;
pub fn wrap() { get_stats(); }
"#,
),
(
"src/rest/handlers.rs",
r#"
use crate::rest::service::wrap;
pub fn post_stats() { wrap(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"transitive coverage should pass, got {findings:?}"
);
}
#[test]
fn test_target_fn_transitive_depth_exceeds_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/shared/helpers.rs",
r#"
use crate::application::stats::get_stats;
pub fn deep_call() { get_stats(); }
"#,
),
(
"src/rest/handlers.rs",
r#"
use crate::shared::helpers::deep_call;
pub fn post_stats() { deep_call(); }
"#,
),
]);
let cp = make_config(1, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
assert_eq!(pairs[0].1, vec!["rest".to_string()]);
}
#[test]
fn test_target_fn_only_called_from_tests_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn cmd_stats() { get_stats(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/rest/tests.rs",
r#"
use crate::application::stats::get_stats;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats() { get_stats(); }
}
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let mut cfg_test = HashSet::new();
cfg_test.insert("src/rest/tests.rs".to_string());
let findings = run_check_b(&ws, &four_layer(), &cp, &cfg_test);
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
assert_eq!(pairs[0].1, vec!["rest".to_string()]);
}
#[test]
fn test_non_pub_target_fn_ignored() {
let ws = build_workspace(&[("src/application/stats.rs", "fn get_stats() {}")]);
let cp = make_config(3, &["cli", "mcp", "rest"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(missing_pairs(&findings).is_empty());
}
#[test]
fn test_method_caller_with_receiver_binding_counts() {
let ws = build_workspace(&[
(
"src/application/session.rs",
r#"
pub struct Session;
impl Session {
pub fn open() -> Self { Session }
pub fn search(&self) {}
}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::Session;
pub fn cmd_search() {
let s = Session::open();
s.search();
}
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::Session;
pub fn handle_search() {
let s = Session::open();
s.search();
}
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"method-call via binding should count, got {findings:?}"
);
}
#[test]
fn test_method_caller_without_binding_ignored() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
pub fn cmd_stats(x: UnknownType) { x.get_stats(); }
"#,
),
]);
let cp = make_config(3, &["cli"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0].1, vec!["cli".to_string()]);
}
#[test]
fn test_target_in_exclude_targets_glob_ignored() {
let ws = build_workspace(&[
("src/application/setup.rs", "pub fn run() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::setup::run;
pub fn cmd_setup() { run(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp", "rest"], &["application::setup::*"]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"glob-excluded target should not produce a finding, got {findings:?}"
);
}
#[test]
fn test_target_not_matching_exclude_glob_still_checked() {
let ws = build_workspace(&[
(
"src/application/setup.rs",
r#"
pub fn run() {}
"#,
),
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
use crate::application::setup::run;
use crate::application::stats::get_stats;
pub fn cmd_setup() { run(); }
pub fn cmd_stats() { get_stats(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &["application::setup::*"]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
assert!(pairs[0].0.ends_with("get_stats"));
assert_eq!(pairs[0].1, vec!["mcp".to_string()]);
}
#[test]
fn test_exclude_targets_uses_canonical_without_crate_prefix() {
let ws = build_workspace(&[("src/application/setup.rs", "pub fn run() {}")]);
let cp = make_config(3, &["cli", "mcp"], &["application::setup::run"]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(missing_pairs(&findings).is_empty());
}
#[test]
fn test_orphan_target_with_only_dead_target_caller_fires() {
let ws = build_workspace(&[
(
"src/application/admin.rs",
r#"
pub fn admin_purge() {}
pub fn _legacy_wrapper() { admin_purge(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
pub fn cmd_other() {}
"#,
),
(
"src/mcp/handlers.rs",
r#"
pub fn handle_other() {}
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let names: Vec<String> = missing_pairs(&findings)
.into_iter()
.map(|(t, _)| t)
.collect();
assert!(
names.iter().any(|n| n.ends_with("admin_purge")),
"orphan target with only dead target-internal callers must fire, got {names:?}"
);
}
#[test]
fn test_target_reached_transitively_via_target_chain_no_finding() {
let ws = build_workspace(&[
(
"src/application/middleware.rs",
r#"
pub fn record_operation() {}
"#,
),
(
"src/application/session.rs",
r#"
use crate::application::middleware::record_operation;
pub fn search() { record_operation(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let names: Vec<String> = missing_pairs(&findings)
.into_iter()
.map(|(t, _)| t)
.collect();
assert!(
!names.iter().any(|n| n.ends_with("record_operation")),
"transitively-reached target must not fire, got {names:?}"
);
}
#[test]
fn test_target_self_caller_only_still_fires_orphan() {
let ws = build_workspace(&[(
"src/application/admin.rs",
r#"
pub fn admin_purge() { admin_purge(); }
"#,
)]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let names: Vec<String> = missing_pairs(&findings)
.into_iter()
.map(|(t, _)| t)
.collect();
assert!(
names.iter().any(|n| n.ends_with("admin_purge")),
"self-only-caller orphan must still fire, got {names:?}"
);
}
#[test]
fn test_post_boundary_helper_not_flagged_when_transitive_reach_asymmetric() {
let ws = build_workspace(&[
(
"src/application/middleware.rs",
r#"
pub fn record_operation() {}
"#,
),
(
"src/application/session.rs",
r#"
use crate::application::middleware::record_operation;
pub fn search() { record_operation(); }
pub fn admin() {}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::admin;
pub fn handle_admin() { admin(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
let names: Vec<String> = pairs.iter().map(|(t, _)| t.clone()).collect();
assert!(
!names.iter().any(|n| n.ends_with("record_operation")),
"post-boundary helper should not appear in findings, got {pairs:?}"
);
}
#[test]
fn test_internal_application_chain_no_findings() {
let ws = build_workspace(&[
(
"src/application/middleware.rs",
r#"
pub fn impact_count() -> u32 { 0 }
pub fn record_operation() { impact_count(); }
"#,
),
(
"src/application/session.rs",
r#"
use crate::application::middleware::record_operation;
pub fn search() { record_operation(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::search;
pub fn cmd_search() { search(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"deeper application chain should not fire findings, got {findings:?}"
);
}
#[test]
fn test_capability_in_one_adapter_only_fires_for_that_target() {
let ws = build_workspace(&[
(
"src/application/session.rs",
r#"
pub fn search() {}
pub fn admin_purge() {}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::{search, admin_purge};
pub fn cmd_search() { search(); }
pub fn cmd_admin() { admin_purge(); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::search;
pub fn handle_search() { search(); }
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
assert_eq!(pairs.len(), 1, "got {findings:?}");
assert!(
pairs[0].0.ends_with("admin_purge"),
"expected admin_purge finding, got {pairs:?}"
);
assert_eq!(pairs[0].1, vec!["mcp".to_string()]);
}
#[test]
fn test_impl_in_separate_file_matches_receiver_tracked_calls() {
let ws = build_workspace(&[
("src/application/session.rs", "pub struct Session;"),
(
"src/application/session_impls.rs",
r#"
use crate::application::session::Session;
impl Session {
pub fn open() -> Self { Session }
pub fn search(&self) {}
}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::session::Session;
pub fn cmd_search() {
let s = Session::open();
s.search();
}
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::session::Session;
pub fn handle_search() {
let s = Session::open();
s.search();
}
"#,
),
]);
let cp = make_config(3, &["cli", "mcp"], &[]);
let findings = run_check_b(&ws, &four_layer(), &cp, &empty_cfg_test());
assert!(
missing_pairs(&findings).is_empty(),
"cross-file impl via use should match receiver-tracked calls, got {findings:?}"
);
}
fn ports_cp() -> CompiledCallParity {
CompiledCallParity {
adapters: vec!["cli".to_string(), "mcp".to_string()],
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(),
}
}
#[test]
fn check_b_silent_when_anchor_covered_by_all_adapters() {
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(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 findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::handler::Handler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == anchor),
"anchor covered by all adapters must not produce a finding, got {pairs:?}"
);
}
#[test]
fn check_b_silent_for_concrete_impl_when_only_anchor_reached() {
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(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 findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let concrete = "crate::application::logging::LoggingHandler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == concrete),
"concrete impl-method must NOT be flagged as orphan when its anchor is covered; got {pairs:?}"
);
}
#[test]
fn check_b_flags_inherent_method_even_when_same_name_inherited_default_exists() {
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 AppHandler;
impl Handler for AppHandler {}
impl AppHandler { pub fn handle(&self) {} }
"#,
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let inherent = "crate::application::logging::AppHandler::handle";
assert!(
pairs.iter().any(|(target, _)| target == inherent),
"inherent method `AppHandler::handle` must produce a missing-adapter finding; it must not be silently absorbed into the trait anchor; got {pairs:?}"
);
}
#[test]
fn check_b_silent_anchor_when_adapters_call_inherited_default_concretely() {
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 AppHandler;
impl Handler for AppHandler {}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::logging::AppHandler;
pub fn cmd_log() { AppHandler::handle(&AppHandler); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::logging::AppHandler;
pub fn mcp_log() { AppHandler::handle(&AppHandler); }
"#,
),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::handler::Handler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == anchor),
"anchor must be silent when every adapter covers the capability via the inherited-default concrete form; got {pairs:?}"
);
}
#[test]
fn check_b_anchor_finding_excluded_via_impl_path_glob() {
let ws = build_workspace(&[
(
"src/ports/handler.rs",
"pub trait Handler { fn handle(&self); }",
),
(
"src/application/admin.rs",
r#"
use crate::ports::handler::Handler;
pub struct AdminHandler;
impl Handler for AdminHandler { fn handle(&self) {} }
"#,
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let mut cp = ports_cp();
cp.exclude_targets = globset(&["application::admin::*"]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &cp, &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::handler::Handler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == anchor),
"anchor finding must be silenced when exclude_targets matches an impl_method_canonical (`application::admin::*` matches `application::admin::AdminHandler::handle`); got {pairs:?}"
);
}
#[test]
fn check_b_silent_anchor_when_all_adapters_cover_via_direct_concrete() {
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() { LoggingHandler::handle(&LoggingHandler); }
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::application::logging::LoggingHandler;
pub fn mcp_log() { LoggingHandler::handle(&LoggingHandler); }
"#,
),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::handler::Handler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == anchor),
"anchor must be silent when the capability is covered via direct concrete by every adapter; got {pairs:?}"
);
}
#[test]
fn check_b_does_not_skip_concrete_when_an_adapter_calls_it_directly() {
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() {
LoggingHandler::handle(&LoggingHandler);
}
"#,
),
(
"src/mcp/handlers.rs",
r#"
use crate::ports::handler::Handler;
pub fn mcp_dispatch(h: &dyn Handler) { h.handle(); }
"#,
),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let concrete = "crate::application::logging::LoggingHandler::handle";
let concrete_finding = pairs.iter().find(|(t, _)| t == concrete);
assert!(
concrete_finding.is_some(),
"concrete impl-method must NOT be silently skipped when at least one adapter (cli) calls it directly — drift between cli (direct) and mcp (dispatch) must surface; got {pairs:?}"
);
if let Some((_, missing)) = concrete_finding {
assert!(
missing.iter().any(|a| a == "mcp"),
"concrete pass must report mcp as missing (it reaches via dispatch, not direct concrete); got {missing:?}"
);
}
}
#[test]
fn check_b_flags_anchor_orphan_when_no_adapter_reaches_it() {
let ws = build_workspace(&[
(
"src/ports/orphan.rs",
"pub trait Orphan { fn handle(&self); }",
),
(
"src/application/orphan_impl.rs",
r#"
use crate::ports::orphan::Orphan;
pub struct OrphanImpl;
impl Orphan for OrphanImpl { fn handle(&self) {} }
"#,
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::orphan::Orphan::handle";
assert!(
pairs.iter().any(|(target, _)| target == anchor),
"anchor reached by NO adapter must be flagged as orphan, got {pairs:?}"
);
}
#[test]
fn anchor_finding_carries_trait_method_source_line() {
let ws = build_workspace(&[
(
"src/ports/orphan.rs",
"\npub trait Orphan {\n fn handle(&self);\n}\n",
),
(
"src/application/orphan_impl.rs",
r#"
use crate::ports::orphan::Orphan;
pub struct OrphanImpl;
impl Orphan for OrphanImpl { fn handle(&self) {} }
"#,
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let anchor = "crate::ports::orphan::Orphan::handle";
let hit = findings
.iter()
.find(|f| match &f.kind {
ViolationKind::CallParityMissingAdapter { target_fn, .. } => target_fn == anchor,
_ => false,
})
.unwrap_or_else(|| panic!("anchor orphan finding missing, got {findings:?}"));
assert_eq!(
hit.file, "src/ports/orphan.rs",
"anchor finding must carry the trait method's source file, got {hit:?}"
);
assert_eq!(
hit.line, 3,
"anchor finding must carry the trait method's 1-based line number, got {hit:?}"
);
assert!(
hit.line >= 1,
"anchor finding line must be 1-based (>=1), got {}",
hit.line
);
}
#[test]
fn check_b_anchor_only_target_surface_still_inspected() {
let ws = build_workspace(&[
(
"src/application/cap.rs",
"pub trait Cap { fn run(&self) {} }",
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::application::cap::Cap::run";
assert!(
pairs.iter().any(|(target, _)| target == anchor),
"anchor in anchor-only target layer must still fire missing-adapter, got {pairs:?}"
);
}
#[test]
fn check_b_anchor_reached_transitively_via_target_chain_no_finding() {
let ws = build_workspace(&[
(
"src/ports/handler.rs",
"pub trait Handler { fn handle(&self); }",
),
(
"src/application/wires.rs",
r#"
use crate::ports::handler::Handler;
pub struct LoggingHandler;
impl Handler for LoggingHandler { fn handle(&self) {} }
pub fn dispatch(h: &dyn Handler) { h.handle(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::application::wires::{dispatch, LoggingHandler};
pub fn cmd_run() { dispatch(&LoggingHandler); }
"#,
),
("src/mcp/handlers.rs", "pub fn cmd_other() {}"),
]);
let findings = run_check_b(&ws, &ports_app_cli_mcp(), &ports_cp(), &empty_cfg_test());
let pairs = missing_pairs(&findings);
let anchor = "crate::ports::handler::Handler::handle";
assert!(
!pairs.iter().any(|(target, _)| target == anchor),
"anchor reachable transitively via an adapter-touched target fn must be silent (post-boundary plumbing rule), got {pairs:?}"
);
}
#[test]
fn check_b_anchor_inspected_even_when_target_layer_absent_from_pub_fns_map() {
use super::support::borrowed_files;
use crate::adapters::analyzers::architecture::call_parity_rule::build_handler_touchpoints;
use crate::adapters::analyzers::architecture::call_parity_rule::check_b::check_missing_adapter;
use crate::adapters::analyzers::architecture::call_parity_rule::pub_fns::{
collect_pub_fns_by_layer, PubFnInputs,
};
use crate::adapters::analyzers::architecture::call_parity_rule::workspace_graph::build_call_graph;
let ws = build_workspace(&[
(
"src/application/cap.rs",
"pub trait Cap { fn run(&self) {} }",
),
("src/cli/handlers.rs", "pub fn cmd_other() {}"),
("src/mcp/handlers.rs", "pub fn mcp_other() {}"),
]);
let layers = ports_app_cli_mcp();
let cp = ports_cp();
let cfg_test = empty_cfg_test();
let borrowed = borrowed_files(&ws);
let mut pub_fns = collect_pub_fns_by_layer(PubFnInputs {
files: &borrowed,
aliases_per_file: &ws.aliases_per_file,
layers: &layers,
cfg_test_files: &cfg_test,
transparent_wrappers: &cp.transparent_wrappers,
promoted_attributes: &cp.promoted_attributes,
});
let graph = build_call_graph(
&borrowed,
&ws.aliases_per_file,
&cfg_test,
&layers,
&cp.transparent_wrappers,
);
let touchpoints = build_handler_touchpoints(&pub_fns, &graph, &cp);
pub_fns.remove("application");
assert!(
!pub_fns.contains_key("application"),
"test precondition: target layer key must be absent before invoking check_missing_adapter"
);
let findings = check_missing_adapter(&pub_fns, &graph, &touchpoints, &cp);
let pairs = missing_pairs(&findings);
let anchor = "crate::application::cap::Cap::run";
assert!(
pairs.iter().any(|(target, _)| target == anchor),
"with target layer absent from pub_fns_by_layer, anchor enumeration must still run; got {pairs:?}"
);
}