ta_changeset/
milestone_draft.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PhaseSummary {
12 pub draft_id: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub phase_id: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub phase_title: Option<String>,
20 pub artifact_count: usize,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct MilestoneDraft {
29 pub milestone_id: String,
31 pub milestone_title: String,
33 pub source_drafts: Vec<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub milestone_branch: Option<String>,
38 pub phase_summaries: Vec<PhaseSummary>,
40 pub created_at: DateTime<Utc>,
42}
43
44impl MilestoneDraft {
45 pub fn save(&self, workspace_root: &std::path::Path) -> anyhow::Result<()> {
47 let milestones_dir = workspace_root.join(".ta").join("milestones");
48 std::fs::create_dir_all(&milestones_dir)?;
49 let path = milestones_dir.join(format!("{}.json", self.milestone_id));
50 let content = serde_json::to_string_pretty(self)?;
51 std::fs::write(&path, content).map_err(|e| {
52 anyhow::anyhow!(
53 "Failed to write milestone draft to {}: {}",
54 path.display(),
55 e
56 )
57 })
58 }
59
60 pub fn load(workspace_root: &std::path::Path, milestone_id: &str) -> anyhow::Result<Self> {
62 let path = workspace_root
63 .join(".ta")
64 .join("milestones")
65 .join(format!("{}.json", milestone_id));
66 let content = std::fs::read_to_string(&path).map_err(|e| {
67 anyhow::anyhow!(
68 "Milestone draft not found at {}: {}\n\
69 List milestones with: ls .ta/milestones/",
70 path.display(),
71 e
72 )
73 })?;
74 serde_json::from_str(&content).map_err(|e| {
75 anyhow::anyhow!("Failed to parse milestone draft {}: {}", path.display(), e)
76 })
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use tempfile::tempdir;
84
85 fn make_milestone(id: &str, title: &str) -> MilestoneDraft {
86 MilestoneDraft {
87 milestone_id: id.to_string(),
88 milestone_title: title.to_string(),
89 source_drafts: vec!["draft-aaa".to_string(), "draft-bbb".to_string()],
90 milestone_branch: Some("feature/milestone-1".to_string()),
91 phase_summaries: vec![
92 PhaseSummary {
93 draft_id: "draft-aaa".to_string(),
94 phase_id: Some("v0.15.14".to_string()),
95 phase_title: Some("Phase title A".to_string()),
96 artifact_count: 3,
97 },
98 PhaseSummary {
99 draft_id: "draft-bbb".to_string(),
100 phase_id: Some("v0.15.15".to_string()),
101 phase_title: Some("Phase title B".to_string()),
102 artifact_count: 2,
103 },
104 ],
105 created_at: Utc::now(),
106 }
107 }
108
109 #[test]
110 fn milestone_draft_save_load() {
111 let dir = tempdir().unwrap();
112 let milestone = make_milestone("test-milestone-id", "Test Milestone");
113 milestone.save(dir.path()).unwrap();
114
115 let loaded = MilestoneDraft::load(dir.path(), "test-milestone-id").unwrap();
116 assert_eq!(loaded.milestone_id, "test-milestone-id");
117 assert_eq!(loaded.milestone_title, "Test Milestone");
118 assert_eq!(loaded.source_drafts.len(), 2);
119 assert_eq!(loaded.phase_summaries.len(), 2);
120 assert_eq!(loaded.phase_summaries[0].artifact_count, 3);
121 assert_eq!(
122 loaded.milestone_branch.as_deref(),
123 Some("feature/milestone-1")
124 );
125 }
126
127 #[test]
128 fn milestone_draft_roundtrip_json() {
129 let milestone = make_milestone("roundtrip-id", "Roundtrip Milestone");
130 let json = serde_json::to_string_pretty(&milestone).unwrap();
131 let restored: MilestoneDraft = serde_json::from_str(&json).unwrap();
132 assert_eq!(restored.milestone_id, "roundtrip-id");
133 assert_eq!(restored.source_drafts, milestone.source_drafts);
134 assert_eq!(restored.phase_summaries[1].draft_id, "draft-bbb");
135 }
136
137 #[test]
138 fn milestone_draft_load_missing_returns_error() {
139 let dir = tempdir().unwrap();
140 let err = MilestoneDraft::load(dir.path(), "nonexistent-id").unwrap_err();
141 assert!(
142 err.to_string().contains("not found") || err.to_string().contains("No such file"),
143 "Expected not-found error, got: {}",
144 err
145 );
146 }
147}