#![cfg_attr(coverage_nightly, coverage(off))]
use super::git::{extract_ticket_ids, get_current_commit, ticket_file_updated, CommitInfo};
use super::roadmap::{Roadmap, RoadmapError};
use super::ticket::{TicketFile, TicketStatus};
use std::path::Path;
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn update_roadmap_ticket(
roadmap: &mut Roadmap,
ticket_id: &str,
commit_hash: &str,
) -> Result<bool, RoadmapError> {
let mut updated = false;
for sprint in &mut roadmap.sprints {
for ticket in &mut sprint.tickets {
if ticket.id == ticket_id && !ticket.completed {
ticket.completed = true;
ticket.commit = Some(commit_hash.to_string());
updated = true;
break;
}
}
if updated {
break;
}
}
Ok(updated)
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn write_roadmap(roadmap: &Roadmap, path: &Path) -> Result<(), std::io::Error> {
let content = format_roadmap_markdown(roadmap);
std::fs::write(path, content)?;
Ok(())
}
fn format_roadmap_markdown(roadmap: &Roadmap) -> String {
let mut output = String::new();
output.push_str("# PMAT Agent System Roadmap\n\n");
output.push_str(&format!("## 📋 Planned: {}\n\n", roadmap.version));
for sprint in &roadmap.sprints {
let status_marker = if sprint.is_complete() {
"COMPLETE ✅"
} else {
"IN PROGRESS"
};
output.push_str(&format!(
"### Sprint {}: {} ({}) - {}\n",
sprint.number, sprint.name, sprint.duration, status_marker
));
output.push_str(&format!("**Focus:** {}\n\n", sprint.focus));
for ticket in &sprint.tickets {
let checkbox = if ticket.completed { "[x]" } else { "[ ]" };
let commit_ref = if let Some(ref commit) = ticket.commit {
format!(
" (commit: {})",
commit.get(..7.min(commit.len())).unwrap_or(commit)
)
} else {
String::new()
};
output.push_str(&format!(
"- {} {}: {}{}\n",
checkbox, ticket.id, ticket.description, commit_ref
));
}
output.push('\n');
if !sprint.quality_gates.is_empty() {
output.push_str("**Quality Gates:**\n");
for gate in &sprint.quality_gates {
output.push_str(&format!("- {}\n", gate));
}
output.push('\n');
}
}
output
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn update_roadmap_from_commit(
roadmap_path: &Path,
tickets_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let commit = get_current_commit()?;
let ticket_ids = extract_ticket_ids(&commit.message);
if ticket_ids.is_empty() {
return Ok(()); }
let mut roadmap = Roadmap::from_file(roadmap_path)?;
let mut updated = false;
for ticket_id in ticket_ids {
if process_ticket_update(&mut roadmap, &commit, &ticket_id, tickets_dir)? {
updated = true;
}
}
if updated {
write_roadmap(&roadmap, roadmap_path)?;
println!(
"✓ Updated roadmap with commit {}",
commit
.hash
.get(..7.min(commit.hash.len()))
.unwrap_or(&commit.hash)
);
}
Ok(())
}
fn process_ticket_update(
roadmap: &mut Roadmap,
commit: &CommitInfo,
ticket_id: &str,
tickets_dir: &Path,
) -> Result<bool, Box<dyn std::error::Error>> {
if !ticket_file_updated(commit, ticket_id) {
return Ok(false);
}
let ticket_path = tickets_dir.join(format!("{}.md", ticket_id));
let ticket_file = match TicketFile::from_file(&ticket_path) {
Ok(file) => file,
Err(_) => return Ok(false),
};
if !matches!(
ticket_file.status,
TicketStatus::Green | TicketStatus::Complete
) {
return Ok(false);
}
Ok(update_roadmap_ticket(roadmap, ticket_id, &commit.hash)?)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::maintenance::roadmap::{Sprint, SprintStatus, Ticket};
fn create_test_roadmap() -> Roadmap {
Roadmap {
version: "v2.139.0".into(),
sprints: vec![Sprint {
number: 17,
name: "Test Sprint".into(),
focus: "Testing".into(),
status: SprintStatus::InProgress,
duration: "2 days".into(),
tickets: vec![
Ticket {
id: "TICKET-PMAT-5013".into(),
description: "Auto-update hooks".into(),
completed: false,
commit: None,
},
Ticket {
id: "TICKET-PMAT-5014".into(),
description: "Health score".into(),
completed: false,
commit: None,
},
],
quality_gates: vec!["Coverage >80%".into()],
}],
}
}
#[test]
fn test_update_roadmap_ticket_success() {
let mut roadmap = create_test_roadmap();
let updated = update_roadmap_ticket(&mut roadmap, "TICKET-PMAT-5013", "abc1234").unwrap();
assert!(updated);
assert!(roadmap.sprints[0].tickets[0].completed);
assert_eq!(roadmap.sprints[0].tickets[0].commit, Some("abc1234".into()));
}
#[test]
fn test_update_roadmap_ticket_not_found() {
let mut roadmap = create_test_roadmap();
let updated = update_roadmap_ticket(&mut roadmap, "TICKET-PMAT-9999", "abc1234").unwrap();
assert!(!updated);
}
#[test]
fn test_update_roadmap_ticket_already_completed() {
let mut roadmap = create_test_roadmap();
roadmap.sprints[0].tickets[0].completed = true;
roadmap.sprints[0].tickets[0].commit = Some("old123".into());
let updated = update_roadmap_ticket(&mut roadmap, "TICKET-PMAT-5013", "abc1234").unwrap();
assert!(!updated);
assert_eq!(roadmap.sprints[0].tickets[0].commit, Some("old123".into()));
}
#[test]
fn test_format_roadmap_markdown_structure() {
let roadmap = create_test_roadmap();
let markdown = format_roadmap_markdown(&roadmap);
assert!(markdown.contains("# PMAT Agent System Roadmap"));
assert!(markdown.contains("## 📋 Planned: v2.139.0"));
assert!(markdown.contains("Sprint 17"));
assert!(markdown.contains("Test Sprint"));
}
#[test]
fn test_format_roadmap_markdown_uncompleted_ticket() {
let roadmap = create_test_roadmap();
let markdown = format_roadmap_markdown(&roadmap);
assert!(markdown.contains("[ ] TICKET-PMAT-5013"));
assert!(!markdown.contains("(commit:"));
}
#[test]
fn test_format_roadmap_markdown_completed_ticket() {
let mut roadmap = create_test_roadmap();
roadmap.sprints[0].tickets[0].completed = true;
roadmap.sprints[0].tickets[0].commit = Some("abc1234567".into());
let markdown = format_roadmap_markdown(&roadmap);
assert!(markdown.contains("[x] TICKET-PMAT-5013"));
assert!(markdown.contains("(commit: abc1234)"));
}
#[test]
fn test_format_roadmap_markdown_quality_gates() {
let roadmap = create_test_roadmap();
let markdown = format_roadmap_markdown(&roadmap);
assert!(markdown.contains("**Quality Gates:**"));
assert!(markdown.contains("- Coverage >80%"));
}
#[test]
fn test_format_roadmap_markdown_short_commit_hash() {
let mut roadmap = create_test_roadmap();
roadmap.sprints[0].tickets[0].completed = true;
roadmap.sprints[0].tickets[0].commit = Some("abc".into());
let markdown = format_roadmap_markdown(&roadmap);
assert!(markdown.contains("(commit: abc)"));
}
#[test]
fn test_write_roadmap_creates_file() {
use tempfile::NamedTempFile;
let roadmap = create_test_roadmap();
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path().to_path_buf();
drop(temp_file);
write_roadmap(&roadmap, &temp_path).unwrap();
let content = std::fs::read_to_string(&temp_path).unwrap();
assert!(content.contains("# PMAT Agent System Roadmap"));
assert!(content.contains("TICKET-PMAT-5013"));
}
#[test]
fn test_format_roadmap_markdown_sprint_complete_marker() {
let mut roadmap = create_test_roadmap();
for ticket in &mut roadmap.sprints[0].tickets {
ticket.completed = true;
ticket.commit = Some("abcdef1".into());
}
let markdown = format_roadmap_markdown(&roadmap);
assert!(
markdown.contains("COMPLETE ✅"),
"all-tickets-completed sprint must render the COMPLETE ✅ marker"
);
}
fn write_green_ticket(dir: &Path, ticket_id: &str) {
let content = format!(
"# {}: Coverage Fixture\n\n\
**Status**: GREEN\n\
**Priority**: P1\n\
**Complexity**: 3\n\
**Estimated Time**: 1 hour\n\
**Dependencies**: None\n\
**Sprint**: Sprint 17\n\n\
## Objective\n\n\
Exercise process_ticket_update.\n\n\
## Success Criteria\n\n\
- [ ] Criterion one\n",
ticket_id
);
std::fs::write(dir.join(format!("{}.md", ticket_id)), content).unwrap();
}
#[test]
fn test_process_ticket_update_returns_false_when_ticket_file_not_in_commit() {
let tickets_dir = tempfile::tempdir().unwrap();
write_green_ticket(tickets_dir.path(), "TICKET-PMAT-5013");
let mut roadmap = create_test_roadmap();
let commit = CommitInfo {
hash: "deadbee".into(),
message: "random".into(),
files: vec!["src/lib.rs".into()], };
let result = process_ticket_update(
&mut roadmap,
&commit,
"TICKET-PMAT-5013",
tickets_dir.path(),
)
.unwrap();
assert!(!result, "must short-circuit when ticket file not in commit");
assert!(!roadmap.sprints[0].tickets[0].completed);
}
#[test]
fn test_process_ticket_update_returns_false_when_ticket_file_missing() {
let tickets_dir = tempfile::tempdir().unwrap();
let mut roadmap = create_test_roadmap();
let commit = CommitInfo {
hash: "deadbee".into(),
message: "fix: TICKET-PMAT-5013".into(),
files: vec!["docs/tickets/TICKET-PMAT-5013.md".into()],
};
let result = process_ticket_update(
&mut roadmap,
&commit,
"TICKET-PMAT-5013",
tickets_dir.path(),
)
.unwrap();
assert!(
!result,
"missing ticket file must return Ok(false), not error"
);
assert!(!roadmap.sprints[0].tickets[0].completed);
}
#[test]
fn test_process_ticket_update_returns_false_when_status_not_green_or_complete() {
let tickets_dir = tempfile::tempdir().unwrap();
let red_content = "# TICKET-PMAT-5013: RED Fixture\n\n\
**Status**: RED\n\
**Priority**: P1\n\
**Complexity**: 3\n\
**Estimated Time**: 1 hour\n\
**Dependencies**: None\n\
**Sprint**: Sprint 17\n\n\
## Objective\n\n\
Still failing.\n\n\
## Success Criteria\n\n\
- [ ] One\n";
std::fs::write(tickets_dir.path().join("TICKET-PMAT-5013.md"), red_content).unwrap();
let mut roadmap = create_test_roadmap();
let commit = CommitInfo {
hash: "deadbee".into(),
message: "wip: TICKET-PMAT-5013".into(),
files: vec!["docs/tickets/TICKET-PMAT-5013.md".into()],
};
let result = process_ticket_update(
&mut roadmap,
&commit,
"TICKET-PMAT-5013",
tickets_dir.path(),
)
.unwrap();
assert!(!result, "RED ticket must not mark roadmap completed");
assert!(!roadmap.sprints[0].tickets[0].completed);
}
#[test]
fn test_process_ticket_update_success_marks_roadmap_completed() {
let tickets_dir = tempfile::tempdir().unwrap();
write_green_ticket(tickets_dir.path(), "TICKET-PMAT-5013");
let mut roadmap = create_test_roadmap();
let commit = CommitInfo {
hash: "cafef00d".into(),
message: "feat: TICKET-PMAT-5013 done".into(),
files: vec!["docs/tickets/TICKET-PMAT-5013.md".into()],
};
let result = process_ticket_update(
&mut roadmap,
&commit,
"TICKET-PMAT-5013",
tickets_dir.path(),
)
.unwrap();
assert!(result, "GREEN ticket must flip roadmap entry");
assert!(roadmap.sprints[0].tickets[0].completed);
assert_eq!(
roadmap.sprints[0].tickets[0].commit,
Some("cafef00d".into())
);
}
#[test]
fn test_update_multiple_sprints() {
let mut roadmap = Roadmap {
version: "v2.139.0".into(),
sprints: vec![
Sprint {
number: 16,
name: "Sprint 16".into(),
focus: "Scaffolding".into(),
status: SprintStatus::Complete,
duration: "2 days".into(),
tickets: vec![Ticket {
id: "TICKET-PMAT-5001".into(),
description: "Core scaffold".into(),
completed: true,
commit: Some("old123".into()),
}],
quality_gates: vec![],
},
Sprint {
number: 17,
name: "Sprint 17".into(),
focus: "Maintenance".into(),
status: SprintStatus::InProgress,
duration: "3 days".into(),
tickets: vec![Ticket {
id: "TICKET-PMAT-5013".into(),
description: "Auto-update hooks".into(),
completed: false,
commit: None,
}],
quality_gates: vec![],
},
],
};
let updated = update_roadmap_ticket(&mut roadmap, "TICKET-PMAT-5013", "new456").unwrap();
assert!(updated);
assert!(roadmap.sprints[1].tickets[0].completed);
assert_eq!(roadmap.sprints[1].tickets[0].commit, Some("new456".into()));
assert_eq!(roadmap.sprints[0].tickets[0].commit, Some("old123".into()));
}
}