fn generate_next_id(roadmap: &crate::models::roadmap::Roadmap) -> String {
let mut max_num = 0u32;
for item in &roadmap.roadmap {
if let Some(num_str) = item.id.split('-').next_back() {
if let Ok(num) = num_str.parse::<u32>() {
max_num = max_num.max(num);
}
}
}
format!("PMAT-{:03}", max_num + 1)
}
fn find_item_fuzzy(
service: &RoadmapService,
id: &str,
) -> Result<crate::models::roadmap::RoadmapItem> {
if let Ok(Some(item)) = service.find_item(id) {
return Ok(item);
}
let roadmap = service.load()?;
let id_lower = id.to_lowercase();
for item in &roadmap.roadmap {
if item.id.to_lowercase() == id_lower {
return Ok(item.clone());
}
}
let mut matches: Vec<_> = roadmap
.roadmap
.iter()
.filter(|item| item.id.to_lowercase().contains(&id_lower))
.collect();
match matches.len() {
0 => anyhow::bail!(
"Ticket '{}' not found. Use 'pmat work list' to see available tickets.",
id
),
1 => Ok(matches.pop().expect("verified 1 element exists").clone()),
_ => {
let match_ids: Vec<_> = matches.iter().map(|i| i.id.as_str()).collect();
anyhow::bail!(
"Ambiguous ID '{}'. Multiple matches: {}. Please be more specific.",
id,
match_ids.join(", ")
)
}
}
}
fn extract_line_from_yaml_error(error: &str) -> Option<usize> {
if let Some(pos) = error.find("at line ") {
let rest = error.get(pos + 8..).unwrap_or_default();
if let Some(end) = rest.find(' ') {
return rest.get(..end).unwrap_or_default().parse().ok();
}
}
None
}
include!("ticket_validate_migrate.rs");
include!("ticket_crud.rs");
include!("ticket_annotate.rs");
include!("ticket_annotate_output.rs");
include!("ticket_score.rs");
#[cfg(all(test, feature = "broken-tests"))]
#[path = "work_handlers_tests.rs"]
mod tests;
#[cfg(test)]
mod ticket_handlers_pure_tests {
use super::*;
#[test]
fn test_extract_line_from_yaml_error_finds_at_line_pattern() {
let err = "parse error at line 42 column 5: bad yaml";
assert_eq!(extract_line_from_yaml_error(err), Some(42));
}
#[test]
fn test_extract_line_from_yaml_error_no_at_line_pattern() {
assert_eq!(extract_line_from_yaml_error("generic error"), None);
assert_eq!(extract_line_from_yaml_error(""), None);
}
#[test]
fn test_extract_line_from_yaml_error_at_line_without_space_after() {
let err = "at line 42";
assert_eq!(extract_line_from_yaml_error(err), None);
}
#[test]
fn test_extract_line_from_yaml_error_non_numeric_value() {
let err = "at line abc column 1";
assert_eq!(extract_line_from_yaml_error(err), None);
}
#[test]
fn test_generate_next_id_empty_roadmap_starts_at_001() {
use crate::models::roadmap::Roadmap;
let roadmap = Roadmap::new(None);
assert_eq!(generate_next_id(&roadmap), "PMAT-001");
}
#[test]
fn test_generate_next_id_picks_max_plus_one() {
use crate::models::roadmap::{Roadmap, RoadmapItem};
let mut roadmap = Roadmap::new(None);
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-005".into(), "x".into()));
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-100".into(), "y".into()));
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-042".into(), "z".into()));
assert_eq!(generate_next_id(&roadmap), "PMAT-101");
}
#[test]
fn test_generate_next_id_handles_mixed_id_prefixes() {
use crate::models::roadmap::{Roadmap, RoadmapItem};
let mut roadmap = Roadmap::new(None);
roadmap
.roadmap
.push(RoadmapItem::new("GH-50".into(), "x".into()));
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-007".into(), "y".into()));
assert_eq!(generate_next_id(&roadmap), "PMAT-051");
}
#[test]
fn test_generate_next_id_skips_non_numeric_suffixes() {
use crate::models::roadmap::{Roadmap, RoadmapItem};
let mut roadmap = Roadmap::new(None);
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-XX".into(), "non-num".into()));
roadmap
.roadmap
.push(RoadmapItem::new("PMAT-009".into(), "num".into()));
assert_eq!(generate_next_id(&roadmap), "PMAT-010");
}
#[test]
fn test_generate_next_id_pads_to_3_digits() {
use crate::models::roadmap::Roadmap;
let roadmap = Roadmap::new(None);
let id = generate_next_id(&roadmap);
assert_eq!(id, "PMAT-001");
}
}