use crate::classify::tiers::weighted_sum::{
score_file_paths, score_keywords, score_merge_indicator, score_message_length,
score_ticket_prefix, Cat, WeightedSumClassifier, WeightedSumConfig,
};
use crate::core::models::ClassificationMethod;
fn default_classifier() -> WeightedSumClassifier {
WeightedSumClassifier::new(WeightedSumConfig::default())
}
#[test]
fn keyword_score_bugfix_keywords_dominate_bugfix_category() {
let lower = "fix null pointer regression hotfix";
let scores = score_keywords(lower);
let bugfix_score = scores[Cat::Bugfix.index()];
let feature_score = scores[Cat::Feature.index()];
assert!(
bugfix_score > feature_score,
"bugfix keywords should score higher for Bugfix than Feature, got bugfix={bugfix_score:.3} feature={feature_score:.3}"
);
assert!(bugfix_score > 0.0, "bugfix score must be positive");
}
#[test]
fn keyword_score_feature_keywords_dominate_feature_category() {
let lower = "add implement feature support";
let scores = score_keywords(lower);
let feature_score = scores[Cat::Feature.index()];
let bugfix_score = scores[Cat::Bugfix.index()];
assert!(
feature_score > bugfix_score,
"feature keywords should score higher for Feature, got feature={feature_score:.3} bugfix={bugfix_score:.3}"
);
}
#[test]
fn ticket_prefix_signal_fires_for_jira_prefix() {
let msg = "PROJ-123: update auth module";
let scores = score_ticket_prefix(msg);
for (i, &s) in scores.iter().enumerate() {
assert!(s > 0.0, "category {i} should get a ticket-prefix boost");
}
}
#[test]
fn ticket_prefix_signal_zero_for_no_prefix() {
let msg = "update auth module";
let scores = score_ticket_prefix(msg);
for (i, &s) in scores.iter().enumerate() {
assert_eq!(
s, 0.0,
"category {i} should score 0.0 without ticket prefix"
);
}
}
#[test]
fn length_signal_short_message_nudges_ktlo_not_feature() {
let scores = score_message_length("wip");
assert!(
scores[Cat::Ktlo.index()] > 0.0,
"short message should nudge KTLO"
);
assert!(
scores[Cat::Feature.index()] < 0.0,
"short message should penalise Feature"
);
}
#[test]
fn length_signal_long_message_nudges_feature() {
let long = "add new payment integration with Stripe — supports 3DS, refunds, webhooks, and idempotency keys";
assert!(long.len() > 80, "test message must be >80 chars");
let scores = score_message_length(long);
assert!(
scores[Cat::Feature.index()] > 0.0,
"long message should nudge Feature"
);
}
#[test]
fn merge_indicator_signal_fires_for_is_merge_flag() {
let scores = score_merge_indicator(true, "some message");
assert!(
scores[Cat::Merge.index()] > 0.40,
"merge indicator should give large Merge score"
);
assert!(
scores[Cat::Feature.index()] < 0.0,
"merge indicator should penalise Feature"
);
}
#[test]
fn merge_indicator_signal_zero_for_non_merge() {
let scores = score_merge_indicator(false, "fix null pointer");
for (i, &s) in scores.iter().enumerate() {
assert_eq!(s, 0.0, "non-merge commit should produce 0 for cat {i}");
}
}
#[test]
fn file_paths_signal_zero_when_empty() {
let scores = score_file_paths(&[]);
for (i, &s) in scores.iter().enumerate() {
assert_eq!(s, 0.0, "empty paths should produce 0 for cat {i}");
}
}
#[test]
fn file_paths_signal_tests_heavy_nudges_maintenance() {
let paths: Vec<String> = vec![
"tests/auth_test.rs".to_string(),
"tests/payment_test.rs".to_string(),
"tests/webhook_test.rs".to_string(),
"src/lib.rs".to_string(),
];
let scores = score_file_paths(&paths);
assert!(
scores[Cat::Maintenance.index()] > 0.0,
"tests-heavy paths should boost Maintenance"
);
}
#[test]
fn file_paths_signal_docs_heavy_nudges_content() {
let paths: Vec<String> = vec![
"docs/api.md".to_string(),
"docs/setup.md".to_string(),
"README.md".to_string(),
];
let scores = score_file_paths(&paths);
assert!(
scores[Cat::Content.index()] > 0.0,
"docs-heavy paths should boost Content"
);
}
#[test]
fn integration_fix_message_classifies_as_bugfix() {
let clf = default_classifier();
let result = clf.classify("fix: handle null user — fixes regression", false, &[]);
assert!(result.is_some(), "expected a verdict for a bugfix message");
let r = result.unwrap();
assert_eq!(r.category, "bugfix", "expected bugfix category");
assert!(
r.confidence >= 0.55,
"confidence should be >= 0.55, got {}",
r.confidence
);
assert_eq!(r.method, ClassificationMethod::WeightedSum);
}
#[test]
fn integration_merge_commit_classifies_as_merge() {
let clf = default_classifier();
let result = clf.classify("Merge pull request #42 from main", true, &[]);
assert!(result.is_some(), "expected a verdict for a merge commit");
let r = result.unwrap();
assert_eq!(r.category, "merge");
assert_eq!(r.method, ClassificationMethod::WeightedSum);
}
#[test]
fn integration_feature_message_classifies_as_feature() {
let clf = default_classifier();
let result = clf.classify(
"add new payment feature support with webhook integration",
false,
&[],
);
assert!(result.is_some(), "expected a verdict for a feature message");
let r = result.unwrap();
assert_eq!(r.category, "feature");
assert!(r.confidence >= 0.55);
}
#[test]
fn fall_through_when_no_signal_dominates() {
let clf = default_classifier();
let result = clf.classify("zzz qqq vvv www yyy uuu ppp rrr", false, &[]);
if let Some(ref r) = result {
assert!(
r.confidence >= 0.55,
"if a verdict is emitted it must exceed min_confidence"
);
}
}
#[test]
fn argmax_tie_does_not_emit_verdict() {
let clf = default_classifier();
let result = clf.classify("xyzxyzxyz blah blah blah nothing here", false, &[]);
if let Some(ref r) = result {
assert!(
r.confidence >= clf.config.min_confidence as f64,
"any emitted verdict must clear min_confidence"
);
}
}
#[test]
fn disabled_classifier_always_returns_none() {
let clf = WeightedSumClassifier::new(WeightedSumConfig {
enabled: false,
..WeightedSumConfig::default()
});
let result = clf.classify("fix: handle null pointer — critical bug", false, &[]);
assert!(
result.is_none(),
"disabled classifier must always return None"
);
}
#[test]
fn integration_fix_with_test_paths_produces_bugfix_or_maintenance() {
let clf = default_classifier();
let paths = vec![
"tests/auth_test.rs".to_string(),
"tests/null_test.rs".to_string(),
];
let result = clf.classify("fix bug: handle null pointer in auth module", false, &paths);
assert!(result.is_some(), "expected a verdict");
let r = result.unwrap();
assert!(
r.category == "bugfix" || r.category == "refactor",
"expected bugfix or refactor, got: {}",
r.category
);
assert!(r.confidence >= 0.55);
assert_eq!(r.method, ClassificationMethod::WeightedSum);
}
#[test]
fn emitted_confidence_stays_within_bounds() {
let clf = default_classifier();
let result = clf.classify(
"fix bug issue broken regression hotfix patch resolve repair correct",
false,
&[],
);
if let Some(r) = result {
assert!(r.confidence >= 0.55, "below min_confidence floor");
assert!(r.confidence <= 0.95, "above max confidence ceiling");
}
}