use super::extract::{extract_fields, week_key};
use super::{
classify, is_bot_account, thresholds, word_count, ExclusionReason, PmWorkInput, FORMULA_VERSION,
};
fn jira_payload(reporter: &str, description: &str, created: &str) -> String {
serde_json::json!({
"fields": {
"reporter": { "displayName": reporter },
"description": description,
"created": created,
}
})
.to_string()
}
#[test]
fn classifier_reports_the_v1_formula_version() {
assert_eq!(FORMULA_VERSION, "pm-work-1");
}
#[test]
fn exclusion_reason_round_trips_through_its_wire_string() {
for reason in [
ExclusionReason::None,
ExclusionReason::TerseTitle,
ExclusionReason::AutoGenerated,
ExclusionReason::BotFiled,
] {
let wire = reason.as_wire_str();
assert_eq!(
ExclusionReason::from_wire_str(wire),
Some(reason),
"{wire} must round-trip"
);
}
assert_eq!(
ExclusionReason::from_wire_str("SOMETHING_FROM_V2"),
None,
"an unknown reason must not be read as NONE"
);
}
#[test]
fn terse_title_with_no_body_is_excluded() {
for title in ["Final", "Phase 3", "Historical Occupancy"] {
let verdict = classify(&PmWorkInput {
title,
description: None,
reporter: Some("Fabiana Calabrese"),
human_transitioned: false,
});
assert!(
!verdict.is_meaningful,
"{title:?} must not count as meaningful work"
);
assert_eq!(verdict.exclusion_reason, ExclusionReason::TerseTitle);
}
}
#[test]
fn a_terse_title_with_a_description_body_is_meaningful() {
let verdict = classify(&PmWorkInput {
title: "Phase 3",
description: Some("Roll the pricing model out to the EMEA portfolio."),
reporter: Some("Fabiana Calabrese"),
human_transitioned: false,
});
assert!(verdict.is_meaningful, "a real body rescues a terse title");
assert_eq!(verdict.exclusion_reason, ExclusionReason::None);
assert_eq!(verdict.body_word_count, 9);
}
#[test]
fn terse_title_boundary_is_the_documented_threshold() {
let at_limit = "one two three four five";
assert_eq!(word_count(at_limit), thresholds::TERSE_TITLE_MAX_WORDS);
let over_limit = "one two three four five six";
let terse = classify(&PmWorkInput {
title: at_limit,
..PmWorkInput::default()
});
assert_eq!(terse.exclusion_reason, ExclusionReason::TerseTitle);
let kept = classify(&PmWorkInput {
title: over_limit,
..PmWorkInput::default()
});
assert_eq!(
kept.exclusion_reason,
ExclusionReason::None,
"one word past the threshold must be kept"
);
}
#[test]
fn bot_reporter_with_no_human_transition_is_auto_generated() {
let verdict = classify(&PmWorkInput {
title: "Bump serde from 1.0.203 to 1.0.204 in the workspace manifest",
description: Some("Automated dependency update opened against main."),
reporter: Some("dependabot[bot]"),
human_transitioned: false,
});
assert!(!verdict.is_meaningful);
assert_eq!(verdict.exclusion_reason, ExclusionReason::AutoGenerated);
}
#[test]
fn bot_reporter_with_human_transition_is_bot_filed() {
let verdict = classify(&PmWorkInput {
title: "Nightly integration suite failed on the pricing service",
description: Some("Six assertions failed in the rate-shop regression pack."),
reporter: Some("Jenkins Automation"),
human_transitioned: true,
});
assert!(!verdict.is_meaningful);
assert_eq!(
verdict.exclusion_reason,
ExclusionReason::BotFiled,
"a human moving a bot-filed ticket is triage, not PM authorship"
);
}
#[test]
fn bot_provenance_outranks_terseness() {
let verdict = classify(&PmWorkInput {
title: "Final",
description: None,
reporter: Some("automation-for-jira"),
human_transitioned: false,
});
assert_eq!(verdict.exclusion_reason, ExclusionReason::AutoGenerated);
}
#[test]
fn bot_detection_matches_automation_accounts() {
for name in [
"dependabot[bot]",
"renovate[bot]",
"github-actions",
"Jenkins Automation",
"svc-jira-sync",
"System",
"Automation for Jira",
"noreply@example.com",
"release-ci",
"deploy-ci",
"ci-runner",
] {
assert!(
is_bot_account(name),
"{name:?} must read as an automation account"
);
}
}
#[test]
fn bot_detection_does_not_misfire_on_human_names() {
for name in [
"Fabiana Calabrese",
"Charles Abbott",
"Talbot Reynolds",
"Roberta Systems-Lee",
"Bo Tran",
"Lucia Moreau",
"Marie-Cindy Okafor",
] {
assert!(!is_bot_account(name), "{name:?} must read as a person");
}
}
#[test]
fn substantive_story_is_meaningful() {
let description = "As a revenue manager I want the occupancy forecast to blend \
the last four comparable weeks so that a single anomalous \
weekend stops dominating next month's rate recommendations.";
let verdict = classify(&PmWorkInput {
title: "Blend the occupancy forecast across four comparable weeks",
description: Some(description),
reporter: Some("Fabiana Calabrese"),
human_transitioned: true,
});
assert!(verdict.is_meaningful);
assert_eq!(verdict.exclusion_reason, ExclusionReason::None);
assert!(
verdict.body_word_count > 20,
"fixture must exercise the >20-word case, got {}",
verdict.body_word_count
);
assert_eq!(verdict.title_word_count, 8);
}
#[test]
fn a_missing_reporter_is_treated_as_human() {
let verdict = classify(&PmWorkInput {
title: "Reconcile the group-booking pace report with the PMS extract",
description: Some("The two disagree by roughly 3% on shoulder dates."),
reporter: None,
human_transitioned: false,
});
assert!(
verdict.is_meaningful,
"an absent reporter is a payload gap, not evidence of automation"
);
}
#[test]
fn extracts_jira_reporter_description_and_created() {
let raw = jira_payload(
"Fabiana Calabrese",
"Blend the last four comparable weeks.",
"2026-01-15T09:30:00.000+0000",
);
let fields = extract_fields(Some(&raw));
assert_eq!(fields.reporter.as_deref(), Some("Fabiana Calabrese"));
assert_eq!(
fields.description.as_deref(),
Some("Blend the last four comparable weeks.")
);
assert_eq!(
fields.created.map(week_key).as_deref(),
Some("2026-W03"),
"JIRA's colon-less offset must parse"
);
}
#[test]
fn flattens_atlassian_document_format_description() {
let raw = serde_json::json!({
"fields": {
"reporter": { "displayName": "Fabiana Calabrese" },
"description": {
"type": "doc",
"version": 1,
"content": [{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Blend the forecast" },
{ "type": "text", "text": "across four weeks." }
]
}]
}
}
})
.to_string();
let fields = extract_fields(Some(&raw));
assert_eq!(
fields.description.as_deref(),
Some("Blend the forecast across four weeks."),
"ADF text leaves must flatten to prose"
);
}
#[test]
fn strips_html_from_an_azure_devops_description() {
let raw = serde_json::json!({
"fields": {
"System.CreatedBy": { "displayName": "Charles Abbott" },
"System.Description": "<div><p>Reconcile</p><p>the pace report</p></div>",
"System.CreatedDate": "2026-01-15T09:30:00Z",
}
})
.to_string();
let fields = extract_fields(Some(&raw));
assert_eq!(fields.reporter.as_deref(), Some("Charles Abbott"));
assert_eq!(
fields.description.as_deref().map(word_count),
Some(4),
"HTML tags must not fuse or inflate the word count"
);
}
#[test]
fn extracts_github_body_and_login() {
let raw = serde_json::json!({
"body": "The rate-shop worker retries forever on a 429.",
"user": { "login": "dependabot[bot]" },
"created_at": "2026-01-15T09:30:00Z",
})
.to_string();
let fields = extract_fields(Some(&raw));
assert_eq!(fields.reporter.as_deref(), Some("dependabot[bot]"));
assert_eq!(fields.created.map(week_key).as_deref(), Some("2026-W03"));
assert!(fields.description.is_some());
}
#[test]
fn unparseable_payload_yields_no_fields() {
for raw in [None, Some("not json"), Some("{}")] {
let fields = extract_fields(raw);
assert_eq!(fields.reporter, None);
assert_eq!(fields.description, None);
assert_eq!(fields.created, None);
}
}
#[test]
fn week_key_uses_the_iso_week_numbering_year() {
let at = chrono::DateTime::parse_from_rfc3339("2024-12-31T12:00:00Z")
.expect("parse")
.with_timezone(&chrono::Utc);
assert_eq!(week_key(at), "2025-W01");
}