use crate::mcp_pmcp::tool_functions::{
analyze_deep_context, check_quality_gate_file, check_quality_gates, quality_gate_baseline,
quality_gate_summary,
};
use serde_json::Value;
use std::path::{Path, PathBuf};
fn write(dir: &Path, name: &str, body: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, body).expect("write fixture");
path
}
const ADVISORY_TODO: &str =
"/// Adds.\npub fn add(a: i32, b: i32) -> i32 {\n // TODO: handle overflow\n a + b\n}\n";
const BLOCKING_FIXME: &str =
"/// Adds.\npub fn add(a: i32, b: i32) -> i32 {\n // FIXME: handle overflow\n a + b\n}\n";
fn severities(json: &Value) -> Vec<String> {
json["violations"]
.as_array()
.expect("violations is an array")
.iter()
.map(|v| v["severity"].as_str().unwrap_or_default().to_string())
.collect()
}
fn not_measured(json: &Value) -> Vec<String> {
json["not_measured"]
.as_array()
.expect("not_measured is an array")
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect()
}
#[tokio::test]
async fn a_path_this_gate_cannot_grade_is_named_not_averaged_away() {
let dir = tempfile::tempdir().expect("tempdir");
let graded = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
let ungraded = write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let json = check_quality_gates(&[graded, ungraded.clone()], false)
.await
.expect("quality_gate reports");
assert!(
not_measured(&json).contains(&ungraded.display().to_string()),
"a path the gate could not grade must appear in not_measured, not vanish: {json}"
);
assert_eq!(
json["passed"],
Value::Bool(false),
"half a verdict is not a pass: {json}"
);
let reason = json["violations"]
.as_array()
.expect("violations")
.iter()
.find(|v| v["check_type"] == "not_graded" && v["file"] == ungraded.display().to_string())
.cloned()
.unwrap_or_else(|| panic!("no not_graded row for the ungraded path: {json}"));
assert!(
reason["message"]
.as_str()
.is_some_and(|m| m.contains(".sh")),
"the reason must name what could not be graded: {reason}"
);
}
#[tokio::test]
async fn every_path_the_caller_passed_is_actually_analysed() {
let dir = tempfile::tempdir().expect("tempdir");
let first = write(
dir.path(),
"first.rs",
"/// Doc.\npub fn a() -> i32 { 1 }\n",
);
let second = write(
dir.path(),
"second.rs",
"/// Doc.\npub fn b() -> i32 { 2 }\n",
);
let json = check_quality_gates(&[first, second], false)
.await
.expect("quality_gate reports");
assert_eq!(
json["files_analyzed"], 2,
"two gradable paths in, two graded out: {json}"
);
}
#[tokio::test]
async fn a_blocking_finding_under_a_later_path_still_fails_the_gate() {
let dir = tempfile::tempdir().expect("tempdir");
let clean = write(
dir.path(),
"clean.rs",
"/// Doc.\npub fn a() -> i32 { 1 }\n",
);
let dirty = write(dir.path(), "dirty.rs", BLOCKING_FIXME);
let json = check_quality_gates(&[clean, dirty], false)
.await
.expect("quality_gate reports");
assert_eq!(
json["passed"],
Value::Bool(false),
"a FIXME under paths[1] must fail the gate exactly as it does under paths[0]: {json}"
);
}
#[tokio::test]
async fn the_summary_names_the_files_its_average_left_out() {
let dir = tempfile::tempdir().expect("tempdir");
write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let json = quality_gate_summary(&[dir.path().to_path_buf()])
.await
.expect("summary");
let summary = &json["summary"];
assert_eq!(summary["total_files"], 1, "one of two graded: {json}");
assert!(
not_measured(summary)
.iter()
.any(|entry| entry.ends_with("a.sh")),
"the file the average left out must be named: {json}"
);
assert!(
summary["ungraded_files"]
.as_array()
.expect("ungraded_files")
.iter()
.any(|row| row["reason"].as_str().is_some_and(|r| r.contains(".sh"))),
"…with the reason it was left out: {json}"
);
}
#[tokio::test]
async fn the_summary_measures_every_path_not_just_the_first() {
let dir = tempfile::tempdir().expect("tempdir");
let first = write(
dir.path(),
"first.rs",
"/// Doc.\npub fn a() -> i32 { 1 }\n",
);
let second = write(
dir.path(),
"second.rs",
"/// Doc.\npub fn b() -> i32 { 2 }\n",
);
let json = quality_gate_summary(&[first, second])
.await
.expect("summary");
let summary = &json["summary"];
assert_eq!(
summary["total_files"], 2,
"two gradable paths in, two summarised out — paths[1..] was dropped: {json}"
);
assert_eq!(
summary["passed_files"].as_u64().expect("passed_files"),
2,
"both clean files must be counted: {json}"
);
}
#[tokio::test]
async fn an_ungradable_file_is_disclosed_by_the_summary_not_an_error() {
let dir = tempfile::tempdir().expect("tempdir");
let script = write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let json = quality_gate_summary(std::slice::from_ref(&script))
.await
.expect("a language TDG does not grade is a hole to disclose, not bad input");
let summary = &json["summary"];
assert!(
not_measured(summary)
.iter()
.any(|entry| entry == &script.display().to_string()),
"the file with no grade must be named: {json}"
);
let gate = check_quality_gates(std::slice::from_ref(&script), false)
.await
.expect("quality_gate reports");
assert!(
not_measured(&gate).contains(&script.display().to_string()),
"quality_gate must disclose it too: {gate}"
);
}
#[tokio::test]
async fn a_path_that_measured_nothing_is_disclosed_even_when_a_sibling_measured() {
let dir = tempfile::tempdir().expect("tempdir");
let graded = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
let empty = dir.path().join("emptydir");
std::fs::create_dir(&empty).expect("mkdir");
let alone = check_quality_gates(std::slice::from_ref(&empty), false)
.await
.expect("quality_gate reports");
assert_eq!(
alone["passed"],
Value::Bool(false),
"baseline: an unmeasured path alone must not pass: {alone}"
);
let together = check_quality_gates(&[graded, empty.clone()], false)
.await
.expect("quality_gate reports");
assert!(
not_measured(&together).contains(&empty.display().to_string()),
"a path that measured nothing must be named even when a sibling did: {together}"
);
assert_eq!(
together["passed"],
Value::Bool(false),
"the verdict for an unmeasured path cannot depend on what else was in \
the list — alone={alone} together={together}"
);
}
#[tokio::test]
async fn the_summary_names_a_path_it_measured_nothing_for() {
let dir = tempfile::tempdir().expect("tempdir");
let graded = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
let empty = dir.path().join("emptydir");
std::fs::create_dir(&empty).expect("mkdir");
let json = quality_gate_summary(&[graded, empty.clone()])
.await
.expect("summary");
let summary = &json["summary"];
assert!(
not_measured(summary).contains(&empty.display().to_string()),
"the summary must name the path its average covers nothing of: {json}"
);
}
#[tokio::test]
async fn the_baseline_records_every_path_not_just_the_first() {
let dir = tempfile::tempdir().expect("tempdir");
let first = write(
dir.path(),
"first.rs",
"/// Doc.\npub fn a() -> i32 { 1 }\n",
);
let second = write(
dir.path(),
"second.rs",
"/// Doc.\npub fn b() -> i32 { 2 }\n",
);
let out = dir.path().join("baseline.json");
let json = quality_gate_baseline(&[first, second], Some(&out))
.await
.expect("baseline");
assert_eq!(
json["baseline"]["summary"]["total_files"], 2,
"two gradable paths in, two recorded — paths[1..] was dropped: {json}"
);
}
#[tokio::test]
async fn the_baseline_names_the_paths_it_could_not_record() {
let dir = tempfile::tempdir().expect("tempdir");
let graded = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
let script = write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let out = dir.path().join("baseline.json");
let json = quality_gate_baseline(&[graded, script.clone()], Some(&out))
.await
.expect("baseline");
let names: Vec<String> = json["baseline"]["not_measured"]
.as_array()
.expect("not_measured is an array")
.iter()
.map(|v| v.as_str().unwrap_or_default().to_string())
.collect();
assert!(
names.contains(&script.display().to_string()),
"the path with no entry must be named: {json}"
);
}
#[tokio::test]
async fn naming_the_same_file_twice_does_not_change_the_score() {
let dir = tempfile::tempdir().expect("tempdir");
let file = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
let once = check_quality_gates(std::slice::from_ref(&file), false)
.await
.expect("quality_gate reports");
let twice = check_quality_gates(&[file.clone(), file.clone()], false)
.await
.expect("quality_gate reports");
assert_eq!(
once["files_analyzed"], twice["files_analyzed"],
"the same file on disk weighs once: once={once} twice={twice}"
);
assert_eq!(
once["score"], twice["score"],
"naming a file twice must not move the average: once={once} twice={twice}"
);
}
#[tokio::test]
async fn one_finding_is_described_the_same_way_whether_the_file_or_its_directory_is_named() {
let dir = tempfile::tempdir().expect("tempdir");
let file = write(dir.path(), "lib.rs", BLOCKING_FIXME);
let by_file = check_quality_gates(std::slice::from_ref(&file), false)
.await
.expect("quality_gate reports");
let by_dir = check_quality_gates(&[dir.path().to_path_buf()], false)
.await
.expect("quality_gate reports");
let satd_rows = |json: &Value| -> Vec<(String, String)> {
json["violations"]
.as_array()
.expect("violations is an array")
.iter()
.filter(|v| v["check_type"] == "satd")
.map(|v| {
(
v["severity"].as_str().unwrap_or_default().to_string(),
v["message"].as_str().unwrap_or_default().to_string(),
)
})
.collect()
};
let named = satd_rows(&by_file);
let walked = satd_rows(&by_dir);
assert!(
!named.is_empty(),
"the FIXME must be reported when the file is named: {by_file}"
);
assert_eq!(
named, walked,
"one detector, one severity scale, one wording — naming the file and \
naming its directory must describe the same finding identically: \
by_file={by_file} by_dir={by_dir}"
);
}
#[tokio::test]
async fn the_reason_a_path_is_not_graded_is_the_one_the_rest_of_the_build_gives() {
let dir = tempfile::tempdir().expect("tempdir");
let graded = write(dir.path(), "ok.rs", "/// Doc.\npub fn a() -> i32 { 1 }\n");
std::fs::create_dir(dir.path().join("tests")).expect("mkdir");
let test_source = write(
&dir.path().join("tests"),
"it.rs",
"/// Doc.\npub fn t() -> i32 { 1 }\n",
);
let script = write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let json = check_quality_gates(&[graded, test_source.clone(), script.clone()], false)
.await
.expect("quality_gate reports");
let message_for = |path: &Path| -> String {
json["violations"]
.as_array()
.expect("violations is an array")
.iter()
.find(|v| v["check_type"] == "not_graded" && v["file"] == path.display().to_string())
.and_then(|v| v["message"].as_str())
.unwrap_or_else(|| panic!("no not_graded row for {}: {json}", path.display()))
.to_string()
};
for path in [&test_source, &script] {
let authority = crate::tdg::analyzer_simple::not_gradable_reason(path)
.unwrap_or_else(|| panic!("{} must be refused by the shared rule", path.display()));
assert_eq!(
message_for(path),
authority,
"the gate must report the build's own refusal, not a sentence of its \
own: {json}"
);
}
assert!(
message_for(&test_source).contains("test"),
"a tests/ file must be refused as test source, not as an ungraded \
language: {json}"
);
assert_eq!(
message_for(&script)
.matches("not part of the score")
.count(),
1,
"the reason must not be stated twice in one message: {json}"
);
}
#[tokio::test]
async fn both_gate_entry_points_quote_the_same_refusal() {
let dir = tempfile::tempdir().expect("tempdir");
let script = write(dir.path(), "a.sh", "#!/bin/sh\na() { echo 1; }\n");
let single = check_quality_gate_file(&script, false)
.await
.expect("file gate reports");
let listed = check_quality_gates(std::slice::from_ref(&script), false)
.await
.expect("quality_gate reports");
let reason = |json: &Value| -> String {
json["violations"]
.as_array()
.expect("violations is an array")
.iter()
.find(|v| v["check_type"] == "not_graded")
.and_then(|v| v["message"].as_str())
.unwrap_or_else(|| panic!("no not_graded row: {json}"))
.to_string()
};
assert_eq!(
reason(&single),
reason(&listed),
"one refusal, one sentence: single={single} listed={listed}"
);
}
#[tokio::test]
async fn an_informational_finding_is_reported_but_does_not_decide_the_verdict() {
let dir = tempfile::tempdir().expect("tempdir");
let file = write(dir.path(), "advisory.rs", ADVISORY_TODO);
let json = check_quality_gates(&[file], false)
.await
.expect("quality_gate reports");
assert_eq!(
severities(&json),
vec!["info".to_string()],
"fixture must produce exactly one advisory finding and nothing else: {json}"
);
assert_eq!(
json["passed"],
Value::Bool(true),
"an informational finding must not flip a verdict: {json}"
);
assert_eq!(
json["blocking_violations"], 0,
"the count that decided the verdict must be stated, not inferred: {json}"
);
assert!(
!json["violations"]
.as_array()
.expect("violations")
.is_empty(),
"not deciding is not the same as hiding — the finding stays on the wire: {json}"
);
}
#[tokio::test]
async fn an_error_severity_finding_still_fails_the_gate() {
let dir = tempfile::tempdir().expect("tempdir");
let file = write(dir.path(), "blocking.rs", BLOCKING_FIXME);
let json = check_quality_gates(&[file], false)
.await
.expect("quality_gate reports");
assert!(
severities(&json).contains(&"error".to_string()),
"fixture must produce an error-severity finding: {json}"
);
assert_eq!(
json["passed"],
Value::Bool(false),
"an actionable finding must still fail: {json}"
);
}
#[tokio::test]
async fn both_entry_points_apply_the_same_severity_rule() {
let dir = tempfile::tempdir().expect("tempdir");
for (name, body) in [("a.rs", ADVISORY_TODO), ("b.rs", BLOCKING_FIXME)] {
let file = write(dir.path(), name, body);
let by_paths = check_quality_gates(std::slice::from_ref(&file), false)
.await
.expect("quality_gate reports");
let by_file = check_quality_gate_file(&file, false)
.await
.expect("quality_gate_file reports");
assert_eq!(
by_paths["passed"], by_file["passed"],
"{name}: one tool, one verdict — paths={by_paths} file={by_file}"
);
assert_eq!(
by_paths["blocking_violations"], by_file["blocking_violations"],
"{name}: the same findings must be verdict-bearing on both entry points"
);
}
}
#[tokio::test]
async fn an_unmeasured_path_is_still_not_a_pass() {
let dir = tempfile::tempdir().expect("tempdir");
let file = write(dir.path(), "a.sh", "echo hi\n");
let json = check_quality_gates(&[file], false)
.await
.expect("quality_gate reports");
assert_eq!(
json["passed"],
Value::Bool(false),
"a gate with no measurement must not pass: {json}"
);
}
fn three_language_dir(dir: &Path) {
write(dir, "a.go", "package main\n\nfunc a() int { return 1 }\n");
write(dir, "app.ts", "export const a = (): number => 1;\n");
write(dir, "main.py", "def a():\n return 1\n");
}
#[tokio::test]
async fn include_patterns_is_refused_rather_than_silently_ignored() {
let dir = tempfile::tempdir().expect("tempdir");
three_language_dir(dir.path());
let paths = vec![dir.path().to_path_buf()];
let unfiltered = analyze_deep_context(&paths, None)
.await
.expect("deep context without a filter still works");
let baseline = unfiltered["results"]["file_count"].clone();
assert_eq!(baseline, 3, "fixture must hold three files: {unfiltered}");
let err = analyze_deep_context(&paths, Some(vec!["*.py".to_string()]))
.await
.expect_err("a filter this pipeline cannot apply must not be accepted in silence");
let message = err.to_string();
assert!(
message.contains("include_patterns"),
"the refusal must name the argument it refused: {message}"
);
assert!(
message.contains("*.py"),
"the refusal must echo what was asked for: {message}"
);
}
#[tokio::test]
async fn an_empty_include_patterns_list_is_not_a_refusal() {
let dir = tempfile::tempdir().expect("tempdir");
three_language_dir(dir.path());
let paths = vec![dir.path().to_path_buf()];
let json = analyze_deep_context(&paths, Some(Vec::new()))
.await
.expect("an empty filter asks for nothing and changes nothing");
assert_eq!(json["results"]["file_count"], 3, "{json}");
}
#[test]
fn the_schema_no_longer_advertises_a_filter_the_pipeline_cannot_apply() {
use pmcp::ToolHandler;
let info = crate::mcp_pmcp::analyze_handlers::AnalyzeDeepContextTool::new()
.metadata()
.expect("analyze_deep_context publishes metadata");
let schema = serde_json::to_value(&info.input_schema).expect("schema serialises");
let properties = schema["properties"]
.as_object()
.expect("inputSchema has properties");
assert!(
!properties.contains_key("include_patterns"),
"an argument the tool refuses must not be advertised as one it takes: {schema}"
);
assert!(
properties.contains_key("paths"),
"…and removing it must not have taken `paths` with it: {schema}"
);
}