use super::support::{
build_workspace, cli_mcp_config, empty_cfg_test, run_check_a, three_layer, Workspace,
};
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
fn assert_no_delegation_fn_names(findings: &[MatchLocation]) -> Vec<String> {
findings
.iter()
.filter_map(|f| match &f.kind {
ViolationKind::CallParityNoDelegation { fn_name, .. } => Some(fn_name.clone()),
_ => None,
})
.collect()
}
#[test]
fn test_adapter_fn_direct_delegation_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();
}
"#,
),
]);
let layers = three_layer();
let cp = cli_mcp_config(3);
let findings = run_check_a(&ws, &layers, &cp, &empty_cfg_test());
assert!(
assert_no_delegation_fn_names(&findings).is_empty(),
"direct delegation should pass, got {findings:?}"
);
}
#[test]
fn test_adapter_fn_inline_impl_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
pub fn cmd_stats() {
let _ = 42;
}
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"inline fn should be flagged, got {names:?}"
);
}
#[test]
fn test_adapter_fn_transitive_delegation_via_helper_passes() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/helpers.rs",
r#"
use crate::application::stats::get_stats;
pub fn prepare() {
get_stats();
}
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::cli::helpers::prepare;
pub fn cmd_stats() {
prepare();
}
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
!names.contains(&"cmd_stats".to_string()),
"transitive delegation at depth 2 should pass, got {names:?}"
);
}
#[test]
fn test_adapter_fn_transitive_depth_exceeds_limit_fails() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/helpers.rs",
r#"
use crate::application::stats::get_stats;
pub fn h4() { get_stats(); }
pub fn h3() { h4(); }
pub fn h2() { h3(); }
pub fn h1() { h2(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::cli::helpers::h1;
pub fn cmd_stats() { h1(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"depth-exceeding chain should flag cmd_stats, got {names:?}"
);
}
#[test]
fn test_call_depth_1_only_direct_calls() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/helpers.rs",
r#"
use crate::application::stats::get_stats;
pub fn helper() { get_stats(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::cli::helpers::helper;
pub fn cmd_stats() { helper(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(1), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"call_depth=1 should flag cmd_stats (helper is not target), got {names:?}"
);
}
#[test]
fn test_adapter_fn_method_call_does_not_count() {
let ws = build_workspace(&[
("src/application/dispatch.rs", "pub fn run_it() {}"),
(
"src/cli/handlers.rs",
r#"
pub fn cmd_stats(disp: UnknownType) {
disp.run();
}
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"method call on unknown type must not count as delegation"
);
}
#[test]
fn test_adapter_fn_cross_adapter_call_does_not_count() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::mcp::handlers::handle_stats;
pub fn cmd_stats() { handle_stats(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(1), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"cross-adapter call at depth 1 must not count, got {names:?}"
);
}
#[test]
fn test_adapter_fn_cross_adapter_call_blocked_even_at_deeper_depth() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/mcp/handlers.rs",
r#"
use crate::application::stats::get_stats;
pub fn handle_stats() { get_stats(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::mcp::handlers::handle_stats;
pub fn cmd_stats() { handle_stats(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(2), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
names.contains(&"cmd_stats".to_string()),
"peer-adapter walks must not inherit touchpoints; expected cmd_stats in {names:?}"
);
}
#[test]
fn test_adapter_fn_cfg_test_file_skipped() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
pub fn cmd_stats() {
let _ = 42;
}
"#,
),
]);
let mut cfg_test = std::collections::HashSet::new();
cfg_test.insert("src/cli/handlers.rs".to_string());
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &cfg_test);
assert!(
findings.is_empty(),
"cfg-test adapter file must not produce findings, got {findings:?}"
);
}
#[test]
fn test_adapter_fn_not_in_any_adapter_layer_ignored() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/application/api.rs",
r#"
pub fn internal_api() {
let _ = 42;
}
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
assert!(
findings.is_empty(),
"non-adapter-layer fn must not be checked"
);
}
#[test]
fn test_finding_line_is_fn_sig_line() {
let src = "\n\n\npub fn cmd_stats() { let _ = 42; }\n";
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
("src/cli/handlers.rs", src),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let finding = findings
.iter()
.find(|f| matches!(f.kind, ViolationKind::CallParityNoDelegation { .. }))
.expect("expected a CallParityNoDelegation finding");
assert_eq!(
finding.line, 4,
"line must anchor on fn sig, got {finding:?}"
);
assert_eq!(finding.file, "src/cli/handlers.rs");
}
#[test]
fn test_unparseable_impl_self_type_does_not_collapse_with_free_fns() {
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 search() { get_stats(); }
impl dyn std::fmt::Debug {
// Not a real impl this analyser understands — self-type
// isn't a plain path, so every method inside must be
// skipped, not recorded as `crate::cli::handlers::*`.
pub fn search(&self) {}
}
pub fn cmd_x() { search(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
!names.contains(&"cmd_x".to_string()),
"free-fn `search` must remain the node `cmd_x` reaches via delegation, got {names:?}"
);
}
#[test]
fn test_convergent_graph_does_not_double_enqueue() {
let ws = build_workspace(&[
(
"src/application/common.rs",
r#"
pub fn common() {}
pub fn a() { common(); }
pub fn b() { common(); }
"#,
),
(
"src/cli/helpers.rs",
r#"
use crate::application::common::{a, b};
pub fn h1() { a(); b(); }
pub fn h2() { a(); b(); }
pub fn h3() { a(); b(); }
"#,
),
(
"src/cli/handlers.rs",
r#"
use crate::cli::helpers::{h1, h2, h3};
pub fn cmd_x() { h1(); h2(); h3(); }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
!names.contains(&"cmd_x".to_string()),
"convergent delegation must still resolve, got {names:?}"
);
}
#[test]
fn check_a_skips_deprecated_handler() {
let ws = build_workspace(&[
("src/application/stats.rs", "pub fn get_stats() {}"),
(
"src/cli/handlers.rs",
r#"
#[deprecated]
pub fn cmd_old() { let _ = 42; }
"#,
),
]);
let findings = run_check_a(&ws, &three_layer(), &cli_mcp_config(3), &empty_cfg_test());
let names = assert_no_delegation_fn_names(&findings);
assert!(
!names.contains(&"cmd_old".to_string()),
"deprecated handler should be excluded from Check A, got {names:?}"
);
}