use super::*;
pub(crate) struct CheckGateEntry {
pub name: String,
pub id: String,
pub blocking: bool,
pub class: &'static str,
pub severity: &'static str,
}
#[derive(Debug, Clone)]
pub(crate) struct DashboardFinding {
pub level: &'static str,
pub check_name: String,
pub check_id: String,
pub message: String,
pub in_diff: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum QualityFailureClass {
Introduced,
Preexisting,
Mixed,
Unclassified,
}
impl QualityFailureClass {
pub(crate) fn as_str(self) -> &'static str {
match self {
QualityFailureClass::Introduced => "introduced",
QualityFailureClass::Preexisting => "pre-existing",
QualityFailureClass::Mixed => "mixed",
QualityFailureClass::Unclassified => "unclassified",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct QualityFailureDetail {
pub name: String,
pub classification: QualityFailureClass,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct QualityFailureSummary {
pub(crate) quality_failures: Vec<String>,
pub(crate) introduced_quality_failures: Vec<String>,
pub(crate) preexisting_quality_failures: Vec<String>,
pub(crate) mixed_quality_failures: Vec<String>,
pub(crate) unclassified_quality_failures: Vec<String>,
pub(crate) details: Vec<QualityFailureDetail>,
}
impl QualityFailureSummary {
pub(crate) fn has_new_failures(&self) -> bool {
!self.introduced_quality_failures.is_empty()
|| !self.mixed_quality_failures.is_empty()
|| !self.unclassified_quality_failures.is_empty()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum MergeDecisionState {
Allow,
AllowWithReview,
Hold,
Block,
}
impl MergeDecisionState {
pub(crate) fn hero_class(self) -> &'static str {
match self {
MergeDecisionState::Allow | MergeDecisionState::AllowWithReview => "merge-allow",
MergeDecisionState::Hold => "merge-hold",
MergeDecisionState::Block => "merge-block",
}
}
pub(crate) fn hero_label(self) -> &'static str {
match self {
MergeDecisionState::Allow => "ALLOW MERGE",
MergeDecisionState::AllowWithReview => "ALLOW WITH REVIEW",
MergeDecisionState::Hold => "HOLD MERGE",
MergeDecisionState::Block => "BLOCK MERGE",
}
}
pub(crate) fn card_badge_class(self) -> &'static str {
match self {
MergeDecisionState::Allow => "mdb-pass",
MergeDecisionState::AllowWithReview | MergeDecisionState::Hold => "mdb-hold",
MergeDecisionState::Block => "mdb-fail",
}
}
pub(crate) fn card_label(self) -> &'static str {
match self {
MergeDecisionState::Allow => "GO",
MergeDecisionState::AllowWithReview => "GO WITH REVIEW",
MergeDecisionState::Hold => "HOLD",
MergeDecisionState::Block => "BLOCK",
}
}
pub(crate) fn gate_label(self) -> &'static str {
match self {
MergeDecisionState::Allow => "MERGE",
MergeDecisionState::AllowWithReview => "MERGE WITH REVIEW",
MergeDecisionState::Hold => "HOLD",
MergeDecisionState::Block => "BLOCK",
}
}
pub(crate) fn card_class(self) -> &'static str {
match self {
MergeDecisionState::Allow => "alert-success",
MergeDecisionState::AllowWithReview | MergeDecisionState::Hold => "alert-warning",
MergeDecisionState::Block => "alert-error",
}
}
}
pub(crate) struct MergeDecisionView {
pub state: MergeDecisionState,
pub reason: String,
pub review_caveats: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct DerivedDecision {
pub verdict: &'static str,
pub allow_merge: bool,
pub recommended_merge: bool,
}
pub(crate) fn derive_decision(
analysis_status: crate::policy::engine::AnalysisStatus,
merge_recommendation: crate::policy::engine::MergeRecommendation,
quality_pass: bool,
) -> DerivedDecision {
let verdict = merge_recommendation.legacy_verdict(analysis_status, quality_pass);
DerivedDecision {
verdict,
allow_merge: verdict == "PASS",
recommended_merge: merge_recommendation.legacy_recommended_merge(),
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct BreakingChangeBreakdown {
pub removed_symbols: usize,
pub signature_changes: usize,
pub new_env_requirements: usize,
pub relocated_symbols: usize,
}
impl BreakingChangeBreakdown {
pub fn has_any(&self) -> bool {
self.removed_symbols > 0 || self.signature_changes > 0 || self.new_env_requirements > 0
}
pub fn summary_parts(&self) -> Vec<String> {
let mut parts = Vec::new();
if self.removed_symbols > 0 {
parts.push(format!(
"{} removed public symbol{}",
self.removed_symbols,
if self.removed_symbols == 1 { "" } else { "s" }
));
}
if self.signature_changes > 0 {
parts.push(format!(
"{} signature change{}",
self.signature_changes,
if self.signature_changes == 1 { "" } else { "s" }
));
}
if self.new_env_requirements > 0 {
parts.push(format!(
"{} new env requirement{}",
self.new_env_requirements,
if self.new_env_requirements == 1 {
""
} else {
"s"
}
));
}
if self.relocated_symbols > 0 {
parts.push(format!(
"{} relocated/re-exported (non-breaking)",
self.relocated_symbols
));
}
parts
}
pub fn summary(&self) -> Option<String> {
let parts = self.summary_parts();
if parts.is_empty() {
None
} else {
Some(parts.join(", "))
}
}
}
pub(crate) fn breaking_change_breakdown(breaking: &[BreakingFinding]) -> BreakingChangeBreakdown {
let removed_symbols = breaking
.iter()
.filter(|f| matches!(&f.kind, BreakingKind::RemovedSymbol { .. }))
.count();
let signature_changes = breaking
.iter()
.filter(|f| matches!(&f.kind, BreakingKind::ChangedSignature { .. }))
.count();
let relocated_symbols = breaking
.iter()
.filter(|f| matches!(&f.kind, BreakingKind::RelocatedSymbol { .. }))
.count();
let new_env_requirements = breaking
.iter()
.filter(|f| matches!(&f.kind, BreakingKind::NewEnvRequirement { .. }))
.count();
BreakingChangeBreakdown {
removed_symbols,
signature_changes,
new_env_requirements,
relocated_symbols,
}
}
pub(crate) fn build_review_caveats(
breaking: &[BreakingFinding],
coverage: &CoverageDelta,
findings_count: usize,
) -> Vec<String> {
let mut caveats = Vec::new();
let breaking_breakdown = breaking_change_breakdown(breaking);
if breaking_breakdown.has_any() {
caveats.push(breaking_breakdown.summary_parts().join(" · "));
}
if coverage.total_source > 0 && coverage.pct < 80 {
let mut coverage_caveat = format!("{}% coverage heuristic", coverage.pct);
if coverage_has_rust_inline_test_blind_spot(coverage) {
coverage_caveat.push_str(" (Rust inline #[cfg(test)] modules may be missed)");
}
caveats.push(coverage_caveat);
}
if !coverage.ghost_tests.is_empty() {
caveats.push(format!(
"{} orphaned test candidate{}",
coverage.ghost_tests.len(),
if coverage.ghost_tests.len() == 1 {
""
} else {
"s"
}
));
}
if findings_count > 0 {
caveats.push(format!(
"{} inline finding{}",
findings_count,
if findings_count == 1 { "" } else { "s" }
));
}
caveats
}
pub(crate) fn rust_quality_review_caveats(
_config: &Config,
_checks: &[CheckResult],
) -> Vec<String> {
Vec::new()
}
pub(crate) fn cargo_audit_review_caveats(checks: &[CheckResult]) -> Vec<String> {
checks
.iter()
.find(|c| c.name.eq_ignore_ascii_case("cargo audit"))
.and_then(|check| cargo_audit_informational_summary(&check.output))
.map(|summary| vec![format!("Cargo audit note: {summary}")])
.unwrap_or_default()
}
pub(crate) fn build_merge_decision_view(
policy_allow_merge: bool,
quality_pass: bool,
recommended_merge: bool,
quality_failures: &[String],
quality_failure_details: &[QualityFailureDetail],
blocking_issues: &[String],
review_caveats: Vec<String>,
) -> MergeDecisionView {
let has_new_quality_failures = quality_failure_details
.iter()
.any(|detail| !matches!(detail.classification, QualityFailureClass::Preexisting))
|| (quality_failure_details.is_empty() && !quality_failures.is_empty());
let state = if !policy_allow_merge {
MergeDecisionState::Block
} else if recommended_merge {
if review_caveats.is_empty() {
MergeDecisionState::Allow
} else {
MergeDecisionState::AllowWithReview
}
} else if has_new_quality_failures {
MergeDecisionState::Hold
} else if !review_caveats.is_empty() {
MergeDecisionState::AllowWithReview
} else {
MergeDecisionState::Hold
};
let reason = match state {
MergeDecisionState::Allow => "All quality gates passed".to_string(),
MergeDecisionState::AllowWithReview => format!(
"{}{} review signal{} need attention",
quality_failure_reason_text(quality_failures, quality_failure_details)
.map(|reason| format!("{reason}; "))
.unwrap_or_else(|| "Quality gates passed, but ".to_string()),
review_caveats.len(),
if review_caveats.len() == 1 { "" } else { "s" }
),
MergeDecisionState::Hold => {
if !quality_pass && !quality_failures.is_empty() {
quality_failure_reason_text(quality_failures, quality_failure_details)
.unwrap_or_else(|| {
format!(
"{} quality check{} failed: {}",
quality_failures.len(),
if quality_failures.len() == 1 { "" } else { "s" },
quality_failures.join(", ")
)
})
} else if !review_caveats.is_empty() {
format!(
"{}review required: {} signal{} need attention",
quality_failure_reason_text(quality_failures, quality_failure_details)
.map(|reason| format!("{reason}; "))
.unwrap_or_default(),
review_caveats.len(),
if review_caveats.len() == 1 { "" } else { "s" }
)
} else {
"Merge not recommended".to_string()
}
}
MergeDecisionState::Block => {
if !blocking_issues.is_empty() {
format!(
"{} blocking issue{} found: {}",
blocking_issues.len(),
if blocking_issues.len() == 1 { "" } else { "s" },
blocking_issues.join(", ")
)
} else if !quality_pass {
"Blocking policy violations detected".to_string()
} else {
"Merge blocked by policy".to_string()
}
}
};
MergeDecisionView {
state,
reason,
review_caveats,
}
}
pub(crate) fn check_id_is_baseline_signal(check_id: &str) -> bool {
matches!(
check_id,
"semgrep_scan" | "eslint" | "stylelint" | "ruff" | "prettier" | "rustfmt" | "cargo_audit"
)
}
#[derive(Debug, Clone)]
pub(crate) struct CleanComparison {
target_is_checkout: bool,
worktree_clean: bool,
current_only: bool,
has_base_diff: bool,
configs_changed: std::collections::BTreeSet<&'static str>,
}
impl CleanComparison {
pub(crate) fn resolve(
config: &Config,
resolved_target: &crate::git::ResolvedRef,
resolved_bases: &[crate::git::ResolvedRef],
worktree_clean: bool,
diffs: &[crate::git::Diff],
) -> Self {
let has_base_diff = has_resolvable_base_diff(resolved_target, resolved_bases);
let configs_changed = changed_tool_config_owners(diffs);
let head = crate::git::Repository::open(&config.repo_root)
.ok()
.and_then(|repo| repo.head_commit_id().ok());
match head {
Some(head) => CleanComparison {
target_is_checkout: head == resolved_target.commit_id,
worktree_clean,
current_only: config.current_only,
has_base_diff,
configs_changed,
},
None => CleanComparison {
target_is_checkout: true,
worktree_clean,
current_only: config.current_only,
has_base_diff,
configs_changed,
},
}
}
pub(crate) fn applies_to(&self, check_id: &str) -> bool {
if self.configs_changed.iter().any(|owner| *owner == check_id) {
return false;
}
if !self.has_base_diff {
return false;
}
if self.current_only {
return false;
}
if self.target_is_checkout {
self.worktree_clean
} else {
check_scans_target_snapshot(check_id)
}
}
#[cfg(test)]
pub(crate) fn for_test(target_is_checkout: bool, worktree_clean: bool) -> Self {
CleanComparison {
target_is_checkout,
worktree_clean,
current_only: false,
has_base_diff: true,
configs_changed: std::collections::BTreeSet::new(),
}
}
#[cfg(test)]
pub(crate) fn for_test_current_only() -> Self {
CleanComparison {
target_is_checkout: true,
worktree_clean: true,
current_only: true,
has_base_diff: true,
configs_changed: std::collections::BTreeSet::new(),
}
}
#[cfg(test)]
pub(crate) fn for_test_no_base_diff() -> Self {
CleanComparison {
target_is_checkout: true,
worktree_clean: true,
current_only: false,
has_base_diff: false,
configs_changed: std::collections::BTreeSet::new(),
}
}
#[cfg(test)]
pub(crate) fn for_test_config_changed(owners: &[&'static str]) -> Self {
CleanComparison {
target_is_checkout: true,
worktree_clean: true,
current_only: false,
has_base_diff: true,
configs_changed: owners.iter().copied().collect(),
}
}
}
fn config_file_owner(basename: &str) -> Option<&'static str> {
match basename {
"rustfmt.toml" | ".rustfmt.toml" => Some("rustfmt"),
"ruff.toml" | ".ruff.toml" | "pyproject.toml" => Some("ruff"),
".eslintrc" | ".eslintrc.js" | ".eslintrc.cjs" | ".eslintrc.json" | ".eslintrc.yaml"
| ".eslintrc.yml" | "eslint.config.js" | "eslint.config.mjs" | "eslint.config.cjs" => {
Some("eslint")
}
".stylelintrc"
| ".stylelintrc.json"
| ".stylelintrc.js"
| ".stylelintrc.yaml"
| ".stylelintrc.yml"
| "stylelint.config.js"
| "stylelint.config.cjs" => Some("stylelint"),
".prettierrc"
| ".prettierrc.json"
| ".prettierrc.js"
| ".prettierrc.cjs"
| ".prettierrc.yaml"
| ".prettierrc.yml"
| "prettier.config.js"
| "prettier.config.cjs" => Some("prettier"),
"semgrep.yml" | "semgrep.yaml" | ".semgrep.yml" | ".semgrep.yaml" => Some("semgrep_scan"),
_ => None,
}
}
fn changed_tool_config_owners(
diffs: &[crate::git::Diff],
) -> std::collections::BTreeSet<&'static str> {
let mut owners = std::collections::BTreeSet::new();
for file in diffs.iter().flat_map(|diff| diff.files.iter()) {
let basename = std::path::Path::new(&file.path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(file.path.as_str());
if let Some(owner) = config_file_owner(basename) {
owners.insert(owner);
}
}
owners
}
fn has_resolvable_base_diff(
resolved_target: &crate::git::ResolvedRef,
resolved_bases: &[crate::git::ResolvedRef],
) -> bool {
resolved_bases
.iter()
.any(|base| base.commit_id != resolved_target.commit_id)
}
fn check_scans_target_snapshot(check_id: &str) -> bool {
matches!(check_id, "semgrep_scan" | "ruff" | "eslint" | "stylelint")
}
pub(crate) fn capture_worktree_clean(repo_root: &std::path::Path) -> bool {
!worktree_has_uncommitted_changes(repo_root)
}
fn worktree_has_uncommitted_changes(repo_root: &std::path::Path) -> bool {
let Ok(repo) = git2::Repository::discover(repo_root) else {
return false;
};
let mut opts = git2::StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(true)
.renames_head_to_index(true);
repo.statuses(Some(&mut opts))
.map(|statuses| !statuses.is_empty())
.unwrap_or(false)
}
pub(crate) fn classify_quality_failure(
check_id: &str,
dashboard_findings: &[DashboardFinding],
clean_comparison: bool,
) -> QualityFailureClass {
let mut saw_in_diff = false;
let mut saw_out_of_diff = false;
let mut saw_unlocated = false;
for finding in dashboard_findings
.iter()
.filter(|finding| finding.check_id == check_id)
{
match finding.in_diff {
Some(true) => saw_in_diff = true,
Some(false) => saw_out_of_diff = true,
None => saw_unlocated = true,
}
}
match (saw_in_diff, saw_out_of_diff) {
(true, true) => QualityFailureClass::Mixed,
(true, false) => QualityFailureClass::Introduced,
(false, true)
if !saw_unlocated && clean_comparison && check_id_is_baseline_signal(check_id) =>
{
QualityFailureClass::Preexisting
}
(false, true) => QualityFailureClass::Unclassified,
(false, false) => QualityFailureClass::Unclassified,
}
}
pub(crate) fn push_quality_failure(
summary: &mut QualityFailureSummary,
name: String,
classification: QualityFailureClass,
) {
summary.quality_failures.push(name.clone());
summary.details.push(QualityFailureDetail {
name: name.clone(),
classification,
});
match classification {
QualityFailureClass::Introduced => summary.introduced_quality_failures.push(name),
QualityFailureClass::Preexisting => summary.preexisting_quality_failures.push(name),
QualityFailureClass::Mixed => summary.mixed_quality_failures.push(name),
QualityFailureClass::Unclassified => summary.unclassified_quality_failures.push(name),
}
}
pub(crate) fn build_quality_failure_summary(
checks: &[CheckResult],
dashboard_findings: &[DashboardFinding],
clean_comparison: &CleanComparison,
) -> QualityFailureSummary {
let mut summary = QualityFailureSummary::default();
for check in checks
.iter()
.filter(|check| quality_downgrade_eligible(check))
{
let check_id = check_id_from_name(&check.name);
let classification = classify_quality_failure(
&check_id,
dashboard_findings,
clean_comparison.applies_to(&check_id),
);
push_quality_failure(&mut summary, check.name.clone(), classification);
}
summary
}
fn quality_downgrade_eligible(check: &CheckResult) -> bool {
check.is_failure()
|| (matches!(check.status, crate::checks::CheckStatus::Warnings)
&& check_id_is_baseline_signal(&check_id_from_name(&check.name)))
}
pub(crate) fn quality_failure_reason_text(
quality_failures: &[String],
quality_failure_details: &[QualityFailureDetail],
) -> Option<String> {
if quality_failures.is_empty() {
return None;
}
if quality_failure_details.is_empty() {
return None;
}
let mut introduced = 0usize;
let mut preexisting = 0usize;
let mut mixed = 0usize;
let mut unclassified = 0usize;
for detail in quality_failure_details {
match detail.classification {
QualityFailureClass::Introduced => introduced += 1,
QualityFailureClass::Preexisting => preexisting += 1,
QualityFailureClass::Mixed => mixed += 1,
QualityFailureClass::Unclassified => unclassified += 1,
}
}
let mut parts = Vec::new();
if introduced > 0 {
parts.push(format!("{} introduced", introduced));
}
if preexisting > 0 {
parts.push(format!("{} pre-existing", preexisting));
}
if mixed > 0 {
parts.push(format!("{} mixed", mixed));
}
if unclassified > 0 {
parts.push(format!("{} unclassified", unclassified));
}
if parts.is_empty() {
return None;
}
Some(format!(
"{} quality check{} failed ({})",
quality_failures.len(),
if quality_failures.len() == 1 { "" } else { "s" },
parts.join(", ")
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn advisory_only_review_required_is_allow_with_review_not_hold() {
let view = build_merge_decision_view(
true, true, false, &[], &[], &[], vec!["3 inline findings".to_string()],
);
assert_eq!(view.state, MergeDecisionState::AllowWithReview);
assert_eq!(view.state.gate_label(), "MERGE WITH REVIEW");
assert_eq!(view.state.card_label(), "GO WITH REVIEW");
}
#[test]
fn new_failure_review_required_stays_hold() {
let view = build_merge_decision_view(
true,
true,
false,
&["clippy".to_string()],
&[QualityFailureDetail {
name: "clippy".to_string(),
classification: QualityFailureClass::Introduced,
}],
&[],
vec!["clippy returned warnings".to_string()],
);
assert_eq!(view.state, MergeDecisionState::Hold);
assert_eq!(view.state.gate_label(), "HOLD");
}
#[test]
fn preexisting_only_failure_is_allow_with_review_not_hold() {
let view = build_merge_decision_view(
true,
true,
false,
&["Semgrep scan".to_string()],
&[QualityFailureDetail {
name: "Semgrep scan".to_string(),
classification: QualityFailureClass::Preexisting,
}],
&[],
vec!["Pre-existing quality failures (not from this diff): Semgrep scan".to_string()],
);
assert_eq!(view.state, MergeDecisionState::AllowWithReview);
assert_eq!(view.state.gate_label(), "MERGE WITH REVIEW");
}
#[test]
fn blocking_policy_violation_is_block() {
let view = build_merge_decision_view(
false,
false,
false,
&["semgrep".to_string()],
&[],
&["secret leak".to_string()],
vec![],
);
assert_eq!(view.state, MergeDecisionState::Block);
}
#[test]
fn clean_approve_is_allow() {
let view = build_merge_decision_view(true, true, true, &[], &[], &[], vec![]);
assert_eq!(view.state, MergeDecisionState::Allow);
assert_eq!(view.state.gate_label(), "MERGE");
}
#[test]
fn derived_decision_fields_are_always_coherent() {
use crate::policy::engine::{AnalysisStatus, MergeRecommendation};
let statuses = [
AnalysisStatus::Complete,
AnalysisStatus::Degraded,
AnalysisStatus::Incomplete,
];
let recs = [
MergeRecommendation::Approve,
MergeRecommendation::ReviewRequired,
MergeRecommendation::Block,
];
for status in statuses {
for rec in recs {
for quality_pass in [true, false] {
let d = derive_decision(status, rec, quality_pass);
assert!(
matches!(d.verdict, "PASS" | "CONDITIONAL" | "BLOCK"),
"unexpected verdict {:?}",
d.verdict
);
assert_eq!(d.allow_merge, d.verdict == "PASS");
if d.allow_merge {
assert_eq!(d.verdict, "PASS");
assert_eq!(rec, MergeRecommendation::Approve);
assert!(
d.recommended_merge,
"PASS implies an approve recommendation"
);
assert_eq!(status, AnalysisStatus::Complete);
assert!(quality_pass, "PASS implies quality passed");
}
assert_eq!(d.verdict == "BLOCK", rec == MergeRecommendation::Block);
assert_eq!(d.recommended_merge, rec == MergeRecommendation::Approve);
}
}
}
}
fn out_of_diff_finding(check_id: &str) -> DashboardFinding {
DashboardFinding {
level: "error",
check_name: check_id.to_string(),
check_id: check_id.to_string(),
message: "finding".to_string(),
in_diff: Some(false),
}
}
fn in_diff_finding(check_id: &str) -> DashboardFinding {
DashboardFinding {
level: "error",
check_name: check_id.to_string(),
check_id: check_id.to_string(),
message: "finding".to_string(),
in_diff: Some(true),
}
}
fn failed_check(name: &str) -> CheckResult {
CheckResult {
name: name.to_string(),
status: crate::checks::CheckStatus::Failed,
duration: std::time::Duration::from_millis(1),
output: String::new(),
cached: false,
provenance: None,
}
}
fn warning_check(name: &str) -> CheckResult {
CheckResult {
name: name.to_string(),
status: crate::checks::CheckStatus::Warnings,
duration: std::time::Duration::from_millis(1),
output: String::new(),
cached: false,
provenance: None,
}
}
#[test]
fn semgrep_out_of_diff_findings_are_preexisting() {
let findings = [out_of_diff_finding("semgrep_scan")];
assert_eq!(
classify_quality_failure("semgrep_scan", &findings, true),
QualityFailureClass::Preexisting
);
}
#[test]
fn dirty_scan_out_of_diff_baseline_signal_is_not_preexisting() {
let findings = [out_of_diff_finding("semgrep_scan")];
assert_eq!(
classify_quality_failure("semgrep_scan", &findings, false),
QualityFailureClass::Unclassified
);
}
fn unlocated_finding(check_id: &str) -> DashboardFinding {
DashboardFinding {
level: "error",
check_name: check_id.to_string(),
check_id: check_id.to_string(),
message: "unlocated finding".to_string(),
in_diff: None,
}
}
#[test]
fn unlocated_plus_out_of_diff_baseline_signal_is_not_preexisting() {
let findings = [
unlocated_finding("semgrep_scan"),
out_of_diff_finding("semgrep_scan"),
];
assert_eq!(
classify_quality_failure("semgrep_scan", &findings, true),
QualityFailureClass::Unclassified
);
}
#[test]
fn all_out_of_diff_baseline_signal_is_preexisting() {
let findings = [
out_of_diff_finding("semgrep_scan"),
out_of_diff_finding("semgrep_scan"),
];
assert_eq!(
classify_quality_failure("semgrep_scan", &findings, true),
QualityFailureClass::Preexisting
);
}
#[test]
fn cargo_test_out_of_diff_findings_are_not_preexisting() {
let findings = [out_of_diff_finding("cargo_test")];
assert_eq!(
classify_quality_failure("cargo_test", &findings, true),
QualityFailureClass::Unclassified
);
}
#[test]
fn baseline_signal_membership_excludes_build_test_typecheck_gates() {
for id in [
"semgrep_scan",
"eslint",
"stylelint",
"ruff",
"prettier",
"rustfmt",
"cargo_audit",
] {
assert!(
check_id_is_baseline_signal(id),
"{id} should be baseline signal"
);
}
for id in [
"cargo_test",
"cargo",
"clippy",
"tsc",
"tests",
"pytest",
"mypy",
] {
assert!(
!check_id_is_baseline_signal(id),
"{id} is a whole-project gate, not a baseline signal"
);
}
}
#[test]
fn failed_cargo_test_out_of_diff_still_counts_as_new_failure() {
let findings = [out_of_diff_finding("cargo_test")];
let summary = build_quality_failure_summary(
&[failed_check("cargo test")],
&findings,
&CleanComparison::for_test(true, true),
);
assert!(summary.preexisting_quality_failures.is_empty());
assert_eq!(summary.unclassified_quality_failures, vec!["cargo test"]);
assert!(summary.has_new_failures());
}
#[test]
fn failed_semgrep_out_of_diff_is_preexisting_and_not_new() {
let findings = [out_of_diff_finding("semgrep_scan")];
let summary = build_quality_failure_summary(
&[failed_check("Semgrep scan")],
&findings,
&CleanComparison::for_test(true, true),
);
assert_eq!(summary.preexisting_quality_failures, vec!["Semgrep scan"]);
assert!(!summary.has_new_failures());
}
#[test]
fn dirty_scan_keeps_out_of_diff_semgrep_failure_as_new() {
let findings = [out_of_diff_finding("semgrep_scan")];
let summary = build_quality_failure_summary(
&[failed_check("Semgrep scan")],
&findings,
&CleanComparison::for_test(true, false),
);
assert!(summary.preexisting_quality_failures.is_empty());
assert_eq!(summary.unclassified_quality_failures, vec!["Semgrep scan"]);
assert!(summary.has_new_failures());
}
#[test]
fn remote_target_downgrades_only_snapshot_scanned_checks() {
let clean = CleanComparison::for_test(false, true);
assert!(
clean.applies_to("semgrep_scan"),
"semgrep scans the target snapshot, downgrade applies"
);
assert!(
clean.applies_to("ruff"),
"ruff scans the target snapshot via plan_check_run, downgrade applies"
);
assert!(
clean.applies_to("eslint"),
"eslint scans the target snapshot via plan_check_run, downgrade applies"
);
assert!(
clean.applies_to("stylelint"),
"stylelint scans the target snapshot via plan_check_run, downgrade applies"
);
assert!(
!clean.applies_to("rustfmt"),
"rustfmt scanned the local checkout, downgrade must not apply"
);
assert!(
!clean.applies_to("cargo_audit"),
"cargo_audit scanned the local checkout, downgrade must not apply"
);
let findings = [out_of_diff_finding("rustfmt")];
let summary = build_quality_failure_summary(
&[warning_check("Rustfmt")],
&findings,
&CleanComparison::for_test(false, true),
);
assert!(
summary.preexisting_quality_failures.is_empty(),
"a local-checkout rustfmt finding is not pre-existing on a remote target"
);
assert_eq!(summary.unclassified_quality_failures, vec!["Rustfmt"]);
let semgrep_findings = [out_of_diff_finding("semgrep_scan")];
let semgrep_summary = build_quality_failure_summary(
&[failed_check("Semgrep scan")],
&semgrep_findings,
&CleanComparison::for_test(false, true),
);
assert_eq!(
semgrep_summary.preexisting_quality_failures,
vec!["Semgrep scan"]
);
}
#[test]
fn local_target_downgrades_all_baseline_signals_when_clean() {
let clean = CleanComparison::for_test(true, true);
for id in ["semgrep_scan", "rustfmt", "ruff", "eslint"] {
assert!(
clean.applies_to(id),
"{id} should downgrade on a clean local target"
);
}
let dirty = CleanComparison::for_test(true, false);
for id in ["semgrep_scan", "rustfmt", "ruff", "eslint"] {
assert!(
!dirty.applies_to(id),
"{id} must not downgrade on a dirty local target"
);
}
}
fn config_diff(path: &str) -> crate::git::Diff {
crate::git::Diff {
base: "main".to_string(),
target: "feature".to_string(),
base_commit_id: "base".to_string(),
target_commit_id: "target".to_string(),
files: vec![crate::git::FileChange {
path: path.to_string(),
status: crate::git::FileStatus::Modified,
additions: 1,
deletions: 0,
}],
stats: crate::git::DiffStats {
files_changed: 1,
additions: 1,
deletions: 0,
copied: 0,
},
commits: vec![],
}
}
#[test]
fn config_file_owner_maps_known_config_basenames() {
assert_eq!(config_file_owner("rustfmt.toml"), Some("rustfmt"));
assert_eq!(config_file_owner(".rustfmt.toml"), Some("rustfmt"));
assert_eq!(config_file_owner("pyproject.toml"), Some("ruff"));
assert_eq!(config_file_owner("ruff.toml"), Some("ruff"));
assert_eq!(config_file_owner(".eslintrc.json"), Some("eslint"));
assert_eq!(config_file_owner("eslint.config.mjs"), Some("eslint"));
assert_eq!(config_file_owner(".stylelintrc"), Some("stylelint"));
assert_eq!(config_file_owner(".prettierrc"), Some("prettier"));
assert_eq!(config_file_owner("semgrep.yml"), Some("semgrep_scan"));
assert_eq!(config_file_owner("Cargo.toml"), None);
assert_eq!(config_file_owner("src/main.rs"), None);
}
#[test]
fn changed_tool_config_owners_matches_nested_config_path() {
let diffs = [config_diff("crates/foo/rustfmt.toml")];
let owners = changed_tool_config_owners(&diffs);
assert!(owners.contains("rustfmt"));
assert_eq!(owners.len(), 1);
assert!(changed_tool_config_owners(&[config_diff("src/lib.rs")]).is_empty());
}
#[test]
fn changed_tool_config_suppresses_only_that_tools_downgrade() {
let clean = CleanComparison::for_test_config_changed(&["rustfmt"]);
assert!(
!clean.applies_to("rustfmt"),
"a changed rustfmt config suppresses the rustfmt downgrade"
);
assert!(clean.applies_to("ruff"));
assert!(clean.applies_to("semgrep_scan"));
let findings = [out_of_diff_finding("rustfmt")];
let summary = build_quality_failure_summary(
&[warning_check("Rustfmt")],
&findings,
&CleanComparison::for_test_config_changed(&["rustfmt"]),
);
assert!(summary.preexisting_quality_failures.is_empty());
assert_eq!(summary.unclassified_quality_failures, vec!["Rustfmt"]);
assert!(summary.has_new_failures());
}
#[test]
fn without_config_change_downgrade_still_fires() {
let findings = [out_of_diff_finding("rustfmt")];
let summary = build_quality_failure_summary(
&[warning_check("Rustfmt")],
&findings,
&CleanComparison::for_test(true, true),
);
assert_eq!(summary.preexisting_quality_failures, vec!["Rustfmt"]);
assert!(!summary.has_new_failures());
}
fn resolved_ref(commit_id: &str) -> crate::git::ResolvedRef {
crate::git::ResolvedRef {
name: commit_id.to_string(),
commit_id: commit_id.to_string(),
is_remote: false,
}
}
#[test]
fn no_resolvable_base_diff_when_bases_empty_or_equal_to_target() {
let target = resolved_ref("aaa111");
assert!(
!has_resolvable_base_diff(&target, &[]),
"no base resolved → no baseline"
);
assert!(
!has_resolvable_base_diff(&target, &[resolved_ref("aaa111")]),
"base == target → no baseline"
);
assert!(
has_resolvable_base_diff(&target, &[resolved_ref("bbb222")]),
"a base distinct from the target IS a real baseline"
);
}
#[test]
fn no_base_diff_blocks_downgrade_even_on_clean_local_checkout() {
let clean = CleanComparison::for_test_no_base_diff();
for id in ["semgrep_scan", "rustfmt", "ruff", "eslint"] {
assert!(
!clean.applies_to(id),
"{id} must not downgrade without a diff baseline"
);
}
let findings = [out_of_diff_finding("semgrep_scan")];
let summary = build_quality_failure_summary(
&[failed_check("Semgrep scan")],
&findings,
&CleanComparison::for_test_no_base_diff(),
);
assert!(
summary.preexisting_quality_failures.is_empty(),
"no baseline means nothing can be proven pre-existing"
);
assert_eq!(summary.unclassified_quality_failures, vec!["Semgrep scan"]);
assert!(summary.has_new_failures());
}
#[test]
fn current_only_never_downgrades_to_preexisting() {
let clean = CleanComparison::for_test_current_only();
assert!(
!clean.applies_to("semgrep_scan"),
"current-only must never downgrade even a baseline signal"
);
assert!(!clean.applies_to("rustfmt"));
let findings = [out_of_diff_finding("semgrep_scan")];
let summary = build_quality_failure_summary(
&[failed_check("Semgrep scan")],
&findings,
&CleanComparison::for_test_current_only(),
);
assert!(
summary.preexisting_quality_failures.is_empty(),
"current-only has no baseline to prove a finding pre-existing"
);
assert_eq!(summary.unclassified_quality_failures, vec!["Semgrep scan"]);
assert!(summary.has_new_failures());
}
#[test]
fn introduced_findings_stay_introduced_for_any_check() {
let findings = [in_diff_finding("cargo_test")];
assert_eq!(
classify_quality_failure("cargo_test", &findings, true),
QualityFailureClass::Introduced
);
}
#[test]
fn capture_worktree_clean_reflects_tree_at_capture_time() {
let tmp = tempfile::tempdir().expect("tempdir");
git2::Repository::init(tmp.path()).expect("init repo");
assert!(
capture_worktree_clean(tmp.path()),
"a freshly initialised repo has a clean tree"
);
std::fs::write(tmp.path().join("prview-output.txt"), b"artifact").expect("write");
assert!(
!capture_worktree_clean(tmp.path()),
"an untracked file makes a fresh read dirty"
);
}
#[test]
fn frozen_clean_value_keeps_downgrade_after_later_writes() {
let frozen_clean = CleanComparison::for_test(true, true);
assert!(frozen_clean.applies_to("rustfmt"));
assert!(frozen_clean.applies_to("semgrep_scan"));
let read_late_dirty = CleanComparison::for_test(true, false);
assert!(
!read_late_dirty.applies_to("rustfmt"),
"a late dirty read would wrongly kill the downgrade"
);
}
}