//! Regression tests for changelog entry parsing — both legacy-slug and new-format //! merge commit subjects must resolve to a human-readable "Name (ID)" entry. /// Parse a single merge commit subject (after stripping the `huskies: merge ` prefix) /// into `(id, type_word, human_name)`. /// /// Returns `None` for subjects that are not recognised merge items. fn parse_changelog_entry(item: &str) -> Option<(String, String, String)> { let item = item.trim(); if item.is_empty() { return None; } // Extract leading numeric ID present in both formats. let id: String = item.chars().take_while(|c| c.is_ascii_digit()).collect(); if id.is_empty() { return None; } // Detect format by the character immediately following the digits. // id contains only ASCII digits so id.len() is a valid char boundary. let rest = item.get(id.len()..).unwrap_or(""); if let Some(space_rest) = rest.strip_prefix(' ') { // New format: "1063 story Human Name Here" let mut words = space_rest.splitn(2, ' '); let type_word = words.next().unwrap_or("story").to_string(); let name = words.next().unwrap_or("").trim().to_string(); if name.is_empty() { return None; } Some((id, type_word, name)) } else if let Some(slug_rest) = rest.strip_prefix('_') { // Legacy slug format: "1063_story_human_name_here" let mut parts = slug_rest.splitn(2, '_'); let type_word = parts.next().unwrap_or("story").to_string(); let slug = parts.next().unwrap_or("").replace('_', " "); if slug.is_empty() { return None; } Some((id, type_word, slug)) } else { None } } /// Format a parsed entry as "Human Name (ID)". fn format_entry(id: &str, name: &str) -> String { let mut chars = name.chars(); let capitalised = match chars.next() { None => String::new(), Some(c) => c.to_uppercase().collect::() + chars.as_str(), }; format!("{capitalised} ({id})") } #[test] fn changelog_new_format_story_resolves_to_name_and_id() { let item = "1063 story Tee pipeline events into gateway context"; let (id, _type_word, name) = parse_changelog_entry(item).expect("should parse new format"); assert_eq!(id, "1063"); assert_eq!( format_entry(&id, &name), "Tee pipeline events into gateway context (1063)" ); } #[test] fn changelog_new_format_bug_resolves_to_name_and_id() { let item = "999 bug Fix the broken auth token"; let (id, type_word, name) = parse_changelog_entry(item).expect("should parse new-format bug"); assert_eq!(id, "999"); assert_eq!(type_word, "bug"); assert_eq!(format_entry(&id, &name), "Fix the broken auth token (999)"); } #[test] fn changelog_new_format_refactor_resolves_to_name_and_id() { let item = "777 refactor Extract config parsing"; let (id, type_word, name) = parse_changelog_entry(item).expect("should parse refactor"); assert_eq!(type_word, "refactor"); assert_eq!(format_entry(&id, &name), "Extract config parsing (777)"); } #[test] fn changelog_legacy_slug_story_resolves_to_name_and_id() { let item = "1063_story_tee_pipeline_events_into_gateway_context"; let (id, _type_word, name) = parse_changelog_entry(item).expect("should parse legacy slug"); assert_eq!(id, "1063"); assert_eq!( format_entry(&id, &name), "Tee pipeline events into gateway context (1063)" ); } #[test] fn changelog_legacy_slug_bug_resolves_to_name_and_id() { let item = "999_bug_fix_the_broken_auth_token"; let (id, type_word, name) = parse_changelog_entry(item).expect("should parse legacy bug slug"); assert_eq!(id, "999"); assert_eq!(type_word, "bug"); assert_eq!(format_entry(&id, &name), "Fix the broken auth token (999)"); } #[test] fn changelog_mixed_fixture_all_entries_have_human_names() { // Fixture: a mix of legacy-slug and new-format subjects (as they appear // after stripping the "huskies: merge " prefix from the git log). let fixture = [ // Legacy slug formats (pre-migration) "1001_story_add_matrix_transport", "1002_bug_fix_crdt_sync_disconnect", "1003_refactor_extract_gateway_config", // New format (post-story-1069) "1050 story Add agent pool auto-assign", "1063 story Tee pipeline events into gateway context", "1064 bug Stop lagged handler re-emitting via same channel", "1065 refactor Move squash merge into own module", ]; for item in &fixture { let result = parse_changelog_entry(item); assert!(result.is_some(), "failed to parse merge subject: {item:?}"); let (id, _type_word, name) = result.unwrap(); let entry = format_entry(&id, &name); // Every entry must contain the numeric ID in parentheses. assert!( entry.contains(&format!("({id})")), "entry missing numeric ID: {entry:?}" ); // Name must not be empty or just whitespace. assert!( !name.trim().is_empty(), "empty human name for item: {item:?}" ); // Name must not be a raw slug (contains underscores as word separators). // (Underscores are OK inside words like "auto-assign" but not as spaces.) assert!( !name.contains('_'), "name still contains underscores (slug not decoded): {name:?}" ); } }