1use crate::agent::{AgentResult, AgentStatus, AgentType, EscalationInfo, ImpactLevel};
4use crate::quality::QualityReport;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Issue {
12 pub number: u64,
14 pub title: String,
16 pub body: String,
18 pub state: IssueStateGithub,
20 pub labels: Vec<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub assignee: Option<String>,
25 pub created_at: chrono::DateTime<chrono::Utc>,
27 pub updated_at: chrono::DateTime<chrono::Utc>,
29 pub url: String,
31}
32
33impl Issue {
34 pub fn validate(&self) -> Result<(), String> {
59 if self.number == 0 {
61 return Err("Issue number must be > 0. \
62 Hint: GitHub issue numbers start at 1"
63 .to_string());
64 }
65
66 if self.title.is_empty() {
68 return Err("Issue title cannot be empty. \
69 Hint: Provide a clear, descriptive title"
70 .to_string());
71 }
72
73 if self.title.len() > 256 {
74 return Err(format!(
75 "Issue title too long ({} characters). Maximum 256 characters allowed. \
76 Hint: Keep titles concise and move details to body",
77 self.title.len()
78 ));
79 }
80
81 if !self.url.starts_with("https://github.com/") {
83 return Err(format!(
84 "Invalid GitHub URL format: '{}'. \
85 Hint: URL must start with 'https://github.com/'",
86 self.url
87 ));
88 }
89
90 if !self.url.contains(&format!("/issues/{}", self.number)) {
92 return Err(format!(
93 "URL {} does not match issue number {}. \
94 Hint: URL should contain '/issues/{}'",
95 self.url, self.number, self.number
96 ));
97 }
98
99 if self.updated_at < self.created_at {
101 return Err(format!(
102 "Invalid timestamps: updated_at ({}) < created_at ({}). \
103 Hint: Ensure updated_at is after created_at",
104 self.updated_at, self.created_at
105 ));
106 }
107
108 Ok(())
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "lowercase")]
115pub enum IssueStateGithub {
116 Open,
118 Closed,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "lowercase")]
125pub enum IssueState {
126 Pending,
128 Analyzing,
130 Implementing,
132 Reviewing,
134 Deploying,
136 Done,
138 Blocked,
140 Failed,
142}
143
144impl IssueState {
145 pub fn to_label(&self) -> &'static str {
147 match self {
148 IssueState::Pending => "📥 state:pending",
149 IssueState::Analyzing => "🔍 state:analyzing",
150 IssueState::Implementing => "🏗️ state:implementing",
151 IssueState::Reviewing => "👀 state:reviewing",
152 IssueState::Deploying => "🚀 state:deploying",
153 IssueState::Done => "✅ state:done",
154 IssueState::Blocked => "🚫 state:blocked",
155 IssueState::Failed => "❌ state:failed",
156 }
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct StateTransition {
163 pub from: IssueState,
165 pub to: IssueState,
167 pub timestamp: chrono::DateTime<chrono::Utc>,
169 pub triggered_by: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub reason: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct AgentExecution {
179 pub agent_type: AgentType,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub task_id: Option<String>,
184 pub start_time: chrono::DateTime<chrono::Utc>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub duration_ms: Option<u64>,
192 pub status: AgentStatus,
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub result: Option<AgentResult>,
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub error: Option<String>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct LabelChange {
205 pub timestamp: chrono::DateTime<chrono::Utc>,
207 pub action: LabelAction,
209 pub label: String,
211 pub performed_by: String,
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum LabelAction {
219 Added,
221 Removed,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct TraceNote {
228 pub timestamp: chrono::DateTime<chrono::Utc>,
230 pub author: String,
232 pub content: String,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub tags: Option<Vec<String>>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct PRResult {
242 pub number: u64,
244 pub url: String,
246 pub state: PRState,
248 pub created_at: chrono::DateTime<chrono::Utc>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "lowercase")]
255pub enum PRState {
256 Draft,
258 Open,
260 Merged,
262 Closed,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct DeploymentResult {
269 pub environment: Environment,
271 pub version: String,
273 pub project_id: String,
275 pub deployment_url: String,
277 pub deployed_at: chrono::DateTime<chrono::Utc>,
279 pub duration_ms: u64,
281 pub status: DeploymentStatus,
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "lowercase")]
288pub enum Environment {
289 Staging,
291 Production,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(rename_all = "snake_case")]
298pub enum DeploymentStatus {
299 Success,
301 Failed,
303 RolledBack,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct IssueAnalysis {
310 pub issue_number: u64,
312 pub issue_type: crate::task::TaskType,
314 pub severity: crate::agent::Severity,
316 pub impact: ImpactLevel,
318 pub assigned_agent: crate::agent::AgentType,
320 pub estimated_duration: u32,
322 pub dependencies: Vec<String>,
324 pub labels: Vec<String>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct IssueTraceLog {
331 pub issue_number: u64,
334 pub issue_title: String,
336 pub issue_url: String,
338
339 pub created_at: chrono::DateTime<chrono::Utc>,
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub closed_at: Option<chrono::DateTime<chrono::Utc>>,
345 pub current_state: IssueState,
347 pub state_transitions: Vec<StateTransition>,
349
350 pub agent_executions: Vec<AgentExecution>,
353
354 pub total_tasks: u32,
357 pub completed_tasks: u32,
359 pub failed_tasks: u32,
361
362 pub label_changes: Vec<LabelChange>,
365 pub current_labels: Vec<String>,
367
368 pub quality_reports: Vec<QualityReport>,
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub final_quality_score: Option<u8>,
374
375 pub pull_requests: Vec<PRResult>,
378
379 pub deployments: Vec<DeploymentResult>,
382
383 pub escalations: Vec<EscalationInfo>,
386
387 pub notes: Vec<TraceNote>,
390
391 pub metadata: IssueMetadata,
394}
395
396impl IssueTraceLog {
397 pub fn validate(&self) -> Result<(), String> {
403 if self.issue_number == 0 {
405 return Err("Issue number must be > 0. \
406 Hint: GitHub issue numbers start at 1"
407 .to_string());
408 }
409
410 if self.issue_title.is_empty() {
412 return Err("Issue title cannot be empty. \
413 Hint: Provide a clear, descriptive title"
414 .to_string());
415 }
416
417 if !self.issue_url.starts_with("https://github.com/") {
419 return Err(format!(
420 "Invalid GitHub URL format: '{}'. \
421 Hint: URL must start with 'https://github.com/'",
422 self.issue_url
423 ));
424 }
425
426 if let Some(closed_at) = self.closed_at {
428 if closed_at < self.created_at {
429 return Err(format!(
430 "Invalid timestamps: closed_at ({}) < created_at ({}). \
431 Hint: Ensure closed_at is after created_at",
432 closed_at, self.created_at
433 ));
434 }
435 }
436
437 let sum = self.completed_tasks + self.failed_tasks;
439 if sum > self.total_tasks {
440 return Err(format!(
441 "Task count inconsistency: completed({}) + failed({}) > total({}). \
442 Hint: Sum of completed and failed tasks cannot exceed total",
443 self.completed_tasks, self.failed_tasks, self.total_tasks
444 ));
445 }
446
447 if let Some(score) = self.final_quality_score {
449 if score > 100 {
450 return Err(format!(
451 "Final quality score out of range: {}. Must be 0-100. \
452 Hint: Quality scores represent percentage (0-100)",
453 score
454 ));
455 }
456 }
457
458 Ok(())
459 }
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct IssueMetadata {
465 pub device_identifier: String,
467 pub session_ids: Vec<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub total_duration_ms: Option<u64>,
472 pub last_updated: chrono::DateTime<chrono::Utc>,
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
485 fn test_issue_state_github_serialization() {
486 let state = IssueStateGithub::Open;
487 let json = serde_json::to_string(&state).unwrap();
488 assert_eq!(json, "\"open\"");
489
490 let state = IssueStateGithub::Closed;
491 let json = serde_json::to_string(&state).unwrap();
492 assert_eq!(json, "\"closed\"");
493 }
494
495 #[test]
496 fn test_issue_state_github_roundtrip() {
497 let states = vec![IssueStateGithub::Open, IssueStateGithub::Closed];
498
499 for state in states {
500 let json = serde_json::to_string(&state).unwrap();
501 let deserialized: IssueStateGithub = serde_json::from_str(&json).unwrap();
502 assert_eq!(state, deserialized);
503 }
504 }
505
506 #[test]
511 fn test_issue_state_to_label() {
512 assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
513 assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
514 assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
515 assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
516 assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
517 assert_eq!(IssueState::Done.to_label(), "✅ state:done");
518 assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
519 assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
520 }
521
522 #[test]
523 fn test_issue_state_serialization() {
524 let state = IssueState::Pending;
525 let json = serde_json::to_string(&state).unwrap();
526 assert_eq!(json, "\"pending\"");
527
528 let state = IssueState::Done;
529 let json = serde_json::to_string(&state).unwrap();
530 assert_eq!(json, "\"done\"");
531 }
532
533 #[test]
534 fn test_issue_state_roundtrip() {
535 let states = vec![
536 IssueState::Pending,
537 IssueState::Analyzing,
538 IssueState::Implementing,
539 IssueState::Reviewing,
540 IssueState::Deploying,
541 IssueState::Done,
542 IssueState::Blocked,
543 IssueState::Failed,
544 ];
545
546 for state in states {
547 let json = serde_json::to_string(&state).unwrap();
548 let deserialized: IssueState = serde_json::from_str(&json).unwrap();
549 assert_eq!(state, deserialized);
550 }
551 }
552
553 #[test]
558 fn test_issue_serialization() {
559 let issue = Issue {
560 number: 123,
561 title: "Test issue".to_string(),
562 body: "Issue body".to_string(),
563 state: IssueStateGithub::Open,
564 labels: vec!["bug".to_string(), "priority:high".to_string()],
565 assignee: Some("user123".to_string()),
566 created_at: chrono::Utc::now(),
567 updated_at: chrono::Utc::now(),
568 url: "https://github.com/user/repo/issues/123".to_string(),
569 };
570
571 let json = serde_json::to_string(&issue).unwrap();
572 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
573 assert_eq!(parsed["number"], 123);
574 assert_eq!(parsed["title"], "Test issue");
575 assert_eq!(parsed["state"], "open");
576 assert_eq!(parsed["assignee"], "user123");
577 }
578
579 #[test]
580 fn test_issue_optional_assignee() {
581 let issue = Issue {
582 number: 456,
583 title: "Unassigned issue".to_string(),
584 body: "".to_string(),
585 state: IssueStateGithub::Closed,
586 labels: vec![],
587 assignee: None,
588 created_at: chrono::Utc::now(),
589 updated_at: chrono::Utc::now(),
590 url: "https://github.com/user/repo/issues/456".to_string(),
591 };
592
593 let json = serde_json::to_string(&issue).unwrap();
594 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
595 assert!(parsed.get("assignee").is_none());
596 }
597
598 #[test]
599 fn test_issue_roundtrip() {
600 let issue = Issue {
601 number: 789,
602 title: "Roundtrip test".to_string(),
603 body: "Test".to_string(),
604 state: IssueStateGithub::Open,
605 labels: vec!["test".to_string()],
606 assignee: Some("tester".to_string()),
607 created_at: chrono::Utc::now(),
608 updated_at: chrono::Utc::now(),
609 url: "https://github.com/user/repo/issues/789".to_string(),
610 };
611
612 let json = serde_json::to_string(&issue).unwrap();
613 let deserialized: Issue = serde_json::from_str(&json).unwrap();
614 assert_eq!(issue.number, deserialized.number);
615 assert_eq!(issue.title, deserialized.title);
616 assert_eq!(issue.state, deserialized.state);
617 }
618
619 #[test]
624 fn test_state_transition_serialization() {
625 let transition = StateTransition {
626 from: IssueState::Pending,
627 to: IssueState::Analyzing,
628 timestamp: chrono::Utc::now(),
629 triggered_by: "CoordinatorAgent".to_string(),
630 reason: Some("Starting analysis".to_string()),
631 };
632
633 let json = serde_json::to_string(&transition).unwrap();
634 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
635 assert_eq!(parsed["from"], "pending");
636 assert_eq!(parsed["to"], "analyzing");
637 assert_eq!(parsed["triggered_by"], "CoordinatorAgent");
638 }
639
640 #[test]
641 fn test_state_transition_roundtrip() {
642 let transition = StateTransition {
643 from: IssueState::Implementing,
644 to: IssueState::Reviewing,
645 timestamp: chrono::Utc::now(),
646 triggered_by: "ReviewAgent".to_string(),
647 reason: None,
648 };
649
650 let json = serde_json::to_string(&transition).unwrap();
651 let deserialized: StateTransition = serde_json::from_str(&json).unwrap();
652 assert_eq!(transition.from, deserialized.from);
653 assert_eq!(transition.to, deserialized.to);
654 }
655
656 #[test]
661 fn test_agent_execution_serialization() {
662 let execution = AgentExecution {
663 agent_type: AgentType::CodeGenAgent,
664 task_id: Some("task-001".to_string()),
665 start_time: chrono::Utc::now(),
666 end_time: Some(chrono::Utc::now()),
667 duration_ms: Some(5000),
668 status: AgentStatus::Completed,
669 result: None,
670 error: None,
671 };
672
673 let json = serde_json::to_string(&execution).unwrap();
674 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
675 assert_eq!(parsed["agent_type"], "CodeGenAgent");
676 assert_eq!(parsed["status"], "completed");
677 assert_eq!(parsed["duration_ms"], 5000);
678 }
679
680 #[test]
681 fn test_agent_execution_with_error() {
682 let execution = AgentExecution {
683 agent_type: AgentType::DeploymentAgent,
684 task_id: Some("task-002".to_string()),
685 start_time: chrono::Utc::now(),
686 end_time: Some(chrono::Utc::now()),
687 duration_ms: Some(1000),
688 status: AgentStatus::Failed,
689 result: None,
690 error: Some("Deployment failed".to_string()),
691 };
692
693 let json = serde_json::to_string(&execution).unwrap();
694 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
695 assert_eq!(parsed["error"], "Deployment failed");
696 }
697
698 #[test]
703 fn test_label_action_serialization() {
704 let action = LabelAction::Added;
705 let json = serde_json::to_string(&action).unwrap();
706 assert_eq!(json, "\"added\"");
707
708 let action = LabelAction::Removed;
709 let json = serde_json::to_string(&action).unwrap();
710 assert_eq!(json, "\"removed\"");
711 }
712
713 #[test]
714 fn test_label_action_roundtrip() {
715 let actions = vec![LabelAction::Added, LabelAction::Removed];
716
717 for action in actions {
718 let json = serde_json::to_string(&action).unwrap();
719 let deserialized: LabelAction = serde_json::from_str(&json).unwrap();
720 assert_eq!(action, deserialized);
721 }
722 }
723
724 #[test]
729 fn test_label_change_serialization() {
730 let change = LabelChange {
731 timestamp: chrono::Utc::now(),
732 action: LabelAction::Added,
733 label: "bug".to_string(),
734 performed_by: "IssueAgent".to_string(),
735 };
736
737 let json = serde_json::to_string(&change).unwrap();
738 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
739 assert_eq!(parsed["action"], "added");
740 assert_eq!(parsed["label"], "bug");
741 }
742
743 #[test]
748 fn test_pr_state_serialization() {
749 let state = PRState::Draft;
750 let json = serde_json::to_string(&state).unwrap();
751 assert_eq!(json, "\"draft\"");
752
753 let state = PRState::Merged;
754 let json = serde_json::to_string(&state).unwrap();
755 assert_eq!(json, "\"merged\"");
756 }
757
758 #[test]
759 fn test_pr_state_roundtrip() {
760 let states = vec![
761 PRState::Draft,
762 PRState::Open,
763 PRState::Merged,
764 PRState::Closed,
765 ];
766
767 for state in states {
768 let json = serde_json::to_string(&state).unwrap();
769 let deserialized: PRState = serde_json::from_str(&json).unwrap();
770 assert_eq!(state, deserialized);
771 }
772 }
773
774 #[test]
779 fn test_pr_result_serialization() {
780 let pr = PRResult {
781 number: 42,
782 url: "https://github.com/user/repo/pull/42".to_string(),
783 state: PRState::Open,
784 created_at: chrono::Utc::now(),
785 };
786
787 let json = serde_json::to_string(&pr).unwrap();
788 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
789 assert_eq!(parsed["number"], 42);
790 assert_eq!(parsed["state"], "open");
791 }
792
793 #[test]
798 fn test_environment_serialization() {
799 let env = Environment::Staging;
800 let json = serde_json::to_string(&env).unwrap();
801 assert_eq!(json, "\"staging\"");
802
803 let env = Environment::Production;
804 let json = serde_json::to_string(&env).unwrap();
805 assert_eq!(json, "\"production\"");
806 }
807
808 #[test]
813 fn test_deployment_status_serialization() {
814 let status = DeploymentStatus::Success;
815 let json = serde_json::to_string(&status).unwrap();
816 assert_eq!(json, "\"success\"");
817
818 let status = DeploymentStatus::RolledBack;
819 let json = serde_json::to_string(&status).unwrap();
820 assert_eq!(json, "\"rolled_back\"");
821 }
822
823 #[test]
824 fn test_deployment_status_roundtrip() {
825 let statuses = vec![
826 DeploymentStatus::Success,
827 DeploymentStatus::Failed,
828 DeploymentStatus::RolledBack,
829 ];
830
831 for status in statuses {
832 let json = serde_json::to_string(&status).unwrap();
833 let deserialized: DeploymentStatus = serde_json::from_str(&json).unwrap();
834 assert_eq!(status, deserialized);
835 }
836 }
837
838 #[test]
843 fn test_deployment_result_serialization() {
844 let deployment = DeploymentResult {
845 environment: Environment::Production,
846 version: "v1.2.3".to_string(),
847 project_id: "project-123".to_string(),
848 deployment_url: "https://app.example.com".to_string(),
849 deployed_at: chrono::Utc::now(),
850 duration_ms: 30000,
851 status: DeploymentStatus::Success,
852 };
853
854 let json = serde_json::to_string(&deployment).unwrap();
855 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
856 assert_eq!(parsed["environment"], "production");
857 assert_eq!(parsed["version"], "v1.2.3");
858 assert_eq!(parsed["status"], "success");
859 }
860
861 #[test]
866 fn test_trace_note_serialization() {
867 let note = TraceNote {
868 timestamp: chrono::Utc::now(),
869 author: "user123".to_string(),
870 content: "This is a note".to_string(),
871 tags: Some(vec!["important".to_string()]),
872 };
873
874 let json = serde_json::to_string(¬e).unwrap();
875 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
876 assert_eq!(parsed["content"], "This is a note");
877 assert_eq!(parsed["tags"][0], "important");
878 }
879
880 #[test]
885 fn test_issue_metadata_serialization() {
886 let metadata = IssueMetadata {
887 device_identifier: "MacBook-Pro".to_string(),
888 session_ids: vec!["session-1".to_string(), "session-2".to_string()],
889 total_duration_ms: Some(120000),
890 last_updated: chrono::Utc::now(),
891 };
892
893 let json = serde_json::to_string(&metadata).unwrap();
894 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
895 assert_eq!(parsed["device_identifier"], "MacBook-Pro");
896 assert_eq!(parsed["total_duration_ms"], 120000);
897 }
898
899 #[test]
904 fn test_issue_trace_log_structure() {
905 let metadata = IssueMetadata {
906 device_identifier: "test-device".to_string(),
907 session_ids: vec!["session-1".to_string()],
908 total_duration_ms: Some(60000),
909 last_updated: chrono::Utc::now(),
910 };
911
912 let trace_log = IssueTraceLog {
913 issue_number: 100,
914 issue_title: "Test issue".to_string(),
915 issue_url: "https://github.com/user/repo/issues/100".to_string(),
916 created_at: chrono::Utc::now(),
917 closed_at: None,
918 current_state: IssueState::Implementing,
919 state_transitions: vec![],
920 agent_executions: vec![],
921 total_tasks: 5,
922 completed_tasks: 2,
923 failed_tasks: 0,
924 label_changes: vec![],
925 current_labels: vec!["bug".to_string()],
926 quality_reports: vec![],
927 final_quality_score: None,
928 pull_requests: vec![],
929 deployments: vec![],
930 escalations: vec![],
931 notes: vec![],
932 metadata,
933 };
934
935 let json = serde_json::to_string(&trace_log).unwrap();
936 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
937 assert_eq!(parsed["issue_number"], 100);
938 assert_eq!(parsed["current_state"], "implementing");
939 assert_eq!(parsed["total_tasks"], 5);
940 assert_eq!(parsed["completed_tasks"], 2);
941 }
942
943 #[test]
948 fn test_issue_validate_success() {
949 let issue = Issue {
950 number: 123,
951 title: "Valid issue title".to_string(),
952 body: "Valid body content".to_string(),
953 state: IssueStateGithub::Open,
954 labels: vec![],
955 assignee: None,
956 created_at: chrono::Utc::now(),
957 updated_at: chrono::Utc::now(),
958 url: "https://github.com/owner/repo/issues/123".to_string(),
959 };
960
961 assert!(issue.validate().is_ok());
962 }
963
964 #[test]
965 fn test_issue_validate_zero_number() {
966 let issue = Issue {
967 number: 0,
968 title: "Test".to_string(),
969 body: "Test".to_string(),
970 state: IssueStateGithub::Open,
971 labels: vec![],
972 assignee: None,
973 created_at: chrono::Utc::now(),
974 updated_at: chrono::Utc::now(),
975 url: "https://github.com/owner/repo/issues/0".to_string(),
976 };
977
978 let result = issue.validate();
979 assert!(result.is_err());
980 assert!(result.unwrap_err().contains("Issue number must be > 0"));
981 }
982
983 #[test]
984 fn test_issue_validate_empty_title() {
985 let issue = Issue {
986 number: 123,
987 title: "".to_string(),
988 body: "Test".to_string(),
989 state: IssueStateGithub::Open,
990 labels: vec![],
991 assignee: None,
992 created_at: chrono::Utc::now(),
993 updated_at: chrono::Utc::now(),
994 url: "https://github.com/owner/repo/issues/123".to_string(),
995 };
996
997 let result = issue.validate();
998 assert!(result.is_err());
999 assert!(result.unwrap_err().contains("title cannot be empty"));
1000 }
1001
1002 #[test]
1003 fn test_issue_validate_title_too_long() {
1004 let long_title = "a".repeat(257);
1005 let issue = Issue {
1006 number: 123,
1007 title: long_title,
1008 body: "Test".to_string(),
1009 state: IssueStateGithub::Open,
1010 labels: vec![],
1011 assignee: None,
1012 created_at: chrono::Utc::now(),
1013 updated_at: chrono::Utc::now(),
1014 url: "https://github.com/owner/repo/issues/123".to_string(),
1015 };
1016
1017 let result = issue.validate();
1018 assert!(result.is_err());
1019 assert!(result.unwrap_err().contains("title too long"));
1020 }
1021
1022 #[test]
1023 fn test_issue_validate_invalid_url_format() {
1024 let issue = Issue {
1025 number: 123,
1026 title: "Test".to_string(),
1027 body: "Test".to_string(),
1028 state: IssueStateGithub::Open,
1029 labels: vec![],
1030 assignee: None,
1031 created_at: chrono::Utc::now(),
1032 updated_at: chrono::Utc::now(),
1033 url: "http://example.com/issues/123".to_string(),
1034 };
1035
1036 let result = issue.validate();
1037 assert!(result.is_err());
1038 assert!(result.unwrap_err().contains("Invalid GitHub URL format"));
1039 }
1040
1041 #[test]
1042 fn test_issue_validate_url_number_mismatch() {
1043 let issue = Issue {
1044 number: 123,
1045 title: "Test".to_string(),
1046 body: "Test".to_string(),
1047 state: IssueStateGithub::Open,
1048 labels: vec![],
1049 assignee: None,
1050 created_at: chrono::Utc::now(),
1051 updated_at: chrono::Utc::now(),
1052 url: "https://github.com/owner/repo/issues/456".to_string(),
1053 };
1054
1055 let result = issue.validate();
1056 assert!(result.is_err());
1057 assert!(result.unwrap_err().contains("does not match issue number"));
1058 }
1059
1060 #[test]
1061 fn test_issue_validate_invalid_timestamps() {
1062 let now = chrono::Utc::now();
1063 let future = now + chrono::Duration::hours(1);
1064
1065 let issue = Issue {
1066 number: 123,
1067 title: "Test".to_string(),
1068 body: "Test".to_string(),
1069 state: IssueStateGithub::Open,
1070 labels: vec![],
1071 assignee: None,
1072 created_at: future,
1073 updated_at: now,
1074 url: "https://github.com/owner/repo/issues/123".to_string(),
1075 };
1076
1077 let result = issue.validate();
1078 assert!(result.is_err());
1079 let err_msg = result.unwrap_err();
1080 assert!(err_msg.contains("updated_at") && err_msg.contains("created_at"));
1081 }
1082
1083 #[test]
1084 fn test_issue_validate_title_max_length() {
1085 let title = "a".repeat(256);
1086 let issue = Issue {
1087 number: 123,
1088 title,
1089 body: "Test".to_string(),
1090 state: IssueStateGithub::Open,
1091 labels: vec![],
1092 assignee: None,
1093 created_at: chrono::Utc::now(),
1094 updated_at: chrono::Utc::now(),
1095 url: "https://github.com/owner/repo/issues/123".to_string(),
1096 };
1097
1098 assert!(issue.validate().is_ok());
1099 }
1100
1101 fn create_test_metadata() -> IssueMetadata {
1106 IssueMetadata {
1107 device_identifier: "test-device".to_string(),
1108 session_ids: vec!["session-1".to_string()],
1109 total_duration_ms: Some(60000),
1110 last_updated: chrono::Utc::now(),
1111 }
1112 }
1113
1114 #[test]
1115 fn test_trace_log_validate_success() {
1116 let trace_log = IssueTraceLog {
1117 issue_number: 100,
1118 issue_title: "Test issue".to_string(),
1119 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1120 created_at: chrono::Utc::now(),
1121 closed_at: None,
1122 current_state: IssueState::Implementing,
1123 state_transitions: vec![],
1124 agent_executions: vec![],
1125 total_tasks: 5,
1126 completed_tasks: 2,
1127 failed_tasks: 1,
1128 label_changes: vec![],
1129 current_labels: vec![],
1130 quality_reports: vec![],
1131 final_quality_score: Some(85),
1132 pull_requests: vec![],
1133 deployments: vec![],
1134 escalations: vec![],
1135 notes: vec![],
1136 metadata: create_test_metadata(),
1137 };
1138
1139 assert!(trace_log.validate().is_ok());
1140 }
1141
1142 #[test]
1143 fn test_trace_log_validate_zero_issue_number() {
1144 let trace_log = IssueTraceLog {
1145 issue_number: 0,
1146 issue_title: "Test".to_string(),
1147 issue_url: "https://github.com/user/repo/issues/0".to_string(),
1148 created_at: chrono::Utc::now(),
1149 closed_at: None,
1150 current_state: IssueState::Pending,
1151 state_transitions: vec![],
1152 agent_executions: vec![],
1153 total_tasks: 0,
1154 completed_tasks: 0,
1155 failed_tasks: 0,
1156 label_changes: vec![],
1157 current_labels: vec![],
1158 quality_reports: vec![],
1159 final_quality_score: None,
1160 pull_requests: vec![],
1161 deployments: vec![],
1162 escalations: vec![],
1163 notes: vec![],
1164 metadata: create_test_metadata(),
1165 };
1166
1167 let result = trace_log.validate();
1168 assert!(result.is_err());
1169 assert!(result.unwrap_err().contains("Issue number must be > 0"));
1170 }
1171
1172 #[test]
1173 fn test_trace_log_validate_empty_title() {
1174 let trace_log = IssueTraceLog {
1175 issue_number: 100,
1176 issue_title: "".to_string(),
1177 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1178 created_at: chrono::Utc::now(),
1179 closed_at: None,
1180 current_state: IssueState::Pending,
1181 state_transitions: vec![],
1182 agent_executions: vec![],
1183 total_tasks: 0,
1184 completed_tasks: 0,
1185 failed_tasks: 0,
1186 label_changes: vec![],
1187 current_labels: vec![],
1188 quality_reports: vec![],
1189 final_quality_score: None,
1190 pull_requests: vec![],
1191 deployments: vec![],
1192 escalations: vec![],
1193 notes: vec![],
1194 metadata: create_test_metadata(),
1195 };
1196
1197 let result = trace_log.validate();
1198 assert!(result.is_err());
1199 assert!(result.unwrap_err().contains("title cannot be empty"));
1200 }
1201
1202 #[test]
1203 fn test_trace_log_validate_invalid_url() {
1204 let trace_log = IssueTraceLog {
1205 issue_number: 100,
1206 issue_title: "Test".to_string(),
1207 issue_url: "http://example.com".to_string(),
1208 created_at: chrono::Utc::now(),
1209 closed_at: None,
1210 current_state: IssueState::Pending,
1211 state_transitions: vec![],
1212 agent_executions: vec![],
1213 total_tasks: 0,
1214 completed_tasks: 0,
1215 failed_tasks: 0,
1216 label_changes: vec![],
1217 current_labels: vec![],
1218 quality_reports: vec![],
1219 final_quality_score: None,
1220 pull_requests: vec![],
1221 deployments: vec![],
1222 escalations: vec![],
1223 notes: vec![],
1224 metadata: create_test_metadata(),
1225 };
1226
1227 let result = trace_log.validate();
1228 assert!(result.is_err());
1229 assert!(result.unwrap_err().contains("Invalid GitHub URL format"));
1230 }
1231
1232 #[test]
1233 fn test_trace_log_validate_invalid_timestamps() {
1234 let now = chrono::Utc::now();
1235 let past = now - chrono::Duration::hours(2);
1236
1237 let trace_log = IssueTraceLog {
1238 issue_number: 100,
1239 issue_title: "Test".to_string(),
1240 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1241 created_at: now,
1242 closed_at: Some(past),
1243 current_state: IssueState::Done,
1244 state_transitions: vec![],
1245 agent_executions: vec![],
1246 total_tasks: 0,
1247 completed_tasks: 0,
1248 failed_tasks: 0,
1249 label_changes: vec![],
1250 current_labels: vec![],
1251 quality_reports: vec![],
1252 final_quality_score: None,
1253 pull_requests: vec![],
1254 deployments: vec![],
1255 escalations: vec![],
1256 notes: vec![],
1257 metadata: create_test_metadata(),
1258 };
1259
1260 let result = trace_log.validate();
1261 assert!(result.is_err());
1262 let err_msg = result.unwrap_err();
1263 assert!(err_msg.contains("closed_at") && err_msg.contains("created_at"));
1264 }
1265
1266 #[test]
1267 fn test_trace_log_validate_task_count_inconsistency() {
1268 let trace_log = IssueTraceLog {
1269 issue_number: 100,
1270 issue_title: "Test".to_string(),
1271 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1272 created_at: chrono::Utc::now(),
1273 closed_at: None,
1274 current_state: IssueState::Implementing,
1275 state_transitions: vec![],
1276 agent_executions: vec![],
1277 total_tasks: 5,
1278 completed_tasks: 4,
1279 failed_tasks: 3,
1280 label_changes: vec![],
1281 current_labels: vec![],
1282 quality_reports: vec![],
1283 final_quality_score: None,
1284 pull_requests: vec![],
1285 deployments: vec![],
1286 escalations: vec![],
1287 notes: vec![],
1288 metadata: create_test_metadata(),
1289 };
1290
1291 let result = trace_log.validate();
1292 assert!(result.is_err());
1293 assert!(result.unwrap_err().contains("Task count inconsistency"));
1294 }
1295
1296 #[test]
1297 fn test_trace_log_validate_quality_score_too_high() {
1298 let trace_log = IssueTraceLog {
1299 issue_number: 100,
1300 issue_title: "Test".to_string(),
1301 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1302 created_at: chrono::Utc::now(),
1303 closed_at: None,
1304 current_state: IssueState::Done,
1305 state_transitions: vec![],
1306 agent_executions: vec![],
1307 total_tasks: 0,
1308 completed_tasks: 0,
1309 failed_tasks: 0,
1310 label_changes: vec![],
1311 current_labels: vec![],
1312 quality_reports: vec![],
1313 final_quality_score: Some(101),
1314 pull_requests: vec![],
1315 deployments: vec![],
1316 escalations: vec![],
1317 notes: vec![],
1318 metadata: create_test_metadata(),
1319 };
1320
1321 let result = trace_log.validate();
1322 assert!(result.is_err());
1323 assert!(result.unwrap_err().contains("quality score out of range"));
1324 }
1325
1326 #[test]
1327 fn test_trace_log_validate_quality_score_boundary() {
1328 let trace_log = IssueTraceLog {
1329 issue_number: 100,
1330 issue_title: "Test".to_string(),
1331 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1332 created_at: chrono::Utc::now(),
1333 closed_at: None,
1334 current_state: IssueState::Done,
1335 state_transitions: vec![],
1336 agent_executions: vec![],
1337 total_tasks: 0,
1338 completed_tasks: 0,
1339 failed_tasks: 0,
1340 label_changes: vec![],
1341 current_labels: vec![],
1342 quality_reports: vec![],
1343 final_quality_score: Some(100),
1344 pull_requests: vec![],
1345 deployments: vec![],
1346 escalations: vec![],
1347 notes: vec![],
1348 metadata: create_test_metadata(),
1349 };
1350
1351 assert!(trace_log.validate().is_ok());
1352 }
1353
1354 #[test]
1359 fn test_agent_execution_roundtrip() {
1360 let execution = AgentExecution {
1361 agent_type: AgentType::CodeGenAgent,
1362 task_id: Some("task-001".to_string()),
1363 start_time: chrono::Utc::now(),
1364 end_time: Some(chrono::Utc::now()),
1365 duration_ms: Some(5000),
1366 status: AgentStatus::Completed,
1367 result: None,
1368 error: None,
1369 };
1370
1371 let json = serde_json::to_string(&execution).unwrap();
1372 let deserialized: AgentExecution = serde_json::from_str(&json).unwrap();
1373 assert_eq!(execution.agent_type, deserialized.agent_type);
1374 assert_eq!(execution.duration_ms, deserialized.duration_ms);
1375 }
1376
1377 #[test]
1378 fn test_label_change_roundtrip() {
1379 let change = LabelChange {
1380 timestamp: chrono::Utc::now(),
1381 action: LabelAction::Added,
1382 label: "bug".to_string(),
1383 performed_by: "IssueAgent".to_string(),
1384 };
1385
1386 let json = serde_json::to_string(&change).unwrap();
1387 let deserialized: LabelChange = serde_json::from_str(&json).unwrap();
1388 assert_eq!(change.action, deserialized.action);
1389 assert_eq!(change.label, deserialized.label);
1390 }
1391
1392 #[test]
1393 fn test_pr_result_roundtrip() {
1394 let pr = PRResult {
1395 number: 42,
1396 url: "https://github.com/user/repo/pull/42".to_string(),
1397 state: PRState::Open,
1398 created_at: chrono::Utc::now(),
1399 };
1400
1401 let json = serde_json::to_string(&pr).unwrap();
1402 let deserialized: PRResult = serde_json::from_str(&json).unwrap();
1403 assert_eq!(pr.number, deserialized.number);
1404 assert_eq!(pr.state, deserialized.state);
1405 }
1406
1407 #[test]
1408 fn test_deployment_result_roundtrip() {
1409 let deployment = DeploymentResult {
1410 environment: Environment::Production,
1411 version: "v1.2.3".to_string(),
1412 project_id: "project-123".to_string(),
1413 deployment_url: "https://app.example.com".to_string(),
1414 deployed_at: chrono::Utc::now(),
1415 duration_ms: 30000,
1416 status: DeploymentStatus::Success,
1417 };
1418
1419 let json = serde_json::to_string(&deployment).unwrap();
1420 let deserialized: DeploymentResult = serde_json::from_str(&json).unwrap();
1421 assert_eq!(deployment.environment, deserialized.environment);
1422 assert_eq!(deployment.version, deserialized.version);
1423 }
1424
1425 #[test]
1426 fn test_trace_note_roundtrip() {
1427 let note = TraceNote {
1428 timestamp: chrono::Utc::now(),
1429 author: "user123".to_string(),
1430 content: "This is a note".to_string(),
1431 tags: Some(vec!["important".to_string()]),
1432 };
1433
1434 let json = serde_json::to_string(¬e).unwrap();
1435 let deserialized: TraceNote = serde_json::from_str(&json).unwrap();
1436 assert_eq!(note.author, deserialized.author);
1437 assert_eq!(note.content, deserialized.content);
1438 }
1439
1440 #[test]
1441 fn test_issue_metadata_roundtrip() {
1442 let metadata = IssueMetadata {
1443 device_identifier: "MacBook-Pro".to_string(),
1444 session_ids: vec!["session-1".to_string(), "session-2".to_string()],
1445 total_duration_ms: Some(120000),
1446 last_updated: chrono::Utc::now(),
1447 };
1448
1449 let json = serde_json::to_string(&metadata).unwrap();
1450 let deserialized: IssueMetadata = serde_json::from_str(&json).unwrap();
1451 assert_eq!(metadata.device_identifier, deserialized.device_identifier);
1452 assert_eq!(metadata.session_ids, deserialized.session_ids);
1453 }
1454
1455 #[test]
1456 fn test_issue_analysis_serialization() {
1457 use crate::agent::Severity;
1458 use crate::task::TaskType;
1459
1460 let analysis = IssueAnalysis {
1461 issue_number: 123,
1462 issue_type: TaskType::Feature,
1463 severity: Severity::Medium,
1464 impact: ImpactLevel::High,
1465 assigned_agent: AgentType::CodeGenAgent,
1466 estimated_duration: 120,
1467 dependencies: vec!["#100".to_string(), "#101".to_string()],
1468 labels: vec!["type:feature".to_string(), "priority:high".to_string()],
1469 };
1470
1471 let json = serde_json::to_string(&analysis).unwrap();
1472 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1473 assert_eq!(parsed["issue_number"], 123);
1474 assert_eq!(parsed["estimated_duration"], 120);
1475 assert_eq!(parsed["dependencies"][0], "#100");
1476 }
1477
1478 #[test]
1479 fn test_issue_trace_log_roundtrip() {
1480 let trace_log = IssueTraceLog {
1481 issue_number: 100,
1482 issue_title: "Test issue".to_string(),
1483 issue_url: "https://github.com/user/repo/issues/100".to_string(),
1484 created_at: chrono::Utc::now(),
1485 closed_at: None,
1486 current_state: IssueState::Implementing,
1487 state_transitions: vec![],
1488 agent_executions: vec![],
1489 total_tasks: 5,
1490 completed_tasks: 2,
1491 failed_tasks: 0,
1492 label_changes: vec![],
1493 current_labels: vec!["bug".to_string()],
1494 quality_reports: vec![],
1495 final_quality_score: None,
1496 pull_requests: vec![],
1497 deployments: vec![],
1498 escalations: vec![],
1499 notes: vec![],
1500 metadata: create_test_metadata(),
1501 };
1502
1503 let json = serde_json::to_string(&trace_log).unwrap();
1504 let deserialized: IssueTraceLog = serde_json::from_str(&json).unwrap();
1505 assert_eq!(trace_log.issue_number, deserialized.issue_number);
1506 assert_eq!(trace_log.total_tasks, deserialized.total_tasks);
1507 }
1508
1509 #[test]
1510 fn test_environment_roundtrip() {
1511 let environments = vec![Environment::Staging, Environment::Production];
1512
1513 for env in environments {
1514 let json = serde_json::to_string(&env).unwrap();
1515 let deserialized: Environment = serde_json::from_str(&json).unwrap();
1516 assert_eq!(env, deserialized);
1517 }
1518 }
1519
1520 #[test]
1521 fn test_trace_note_optional_tags() {
1522 let note = TraceNote {
1523 timestamp: chrono::Utc::now(),
1524 author: "user123".to_string(),
1525 content: "Note without tags".to_string(),
1526 tags: None,
1527 };
1528
1529 let json = serde_json::to_string(¬e).unwrap();
1530 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1531 assert!(parsed.get("tags").is_none());
1532 }
1533
1534 #[test]
1535 fn test_issue_metadata_optional_duration() {
1536 let metadata = IssueMetadata {
1537 device_identifier: "test-device".to_string(),
1538 session_ids: vec![],
1539 total_duration_ms: None,
1540 last_updated: chrono::Utc::now(),
1541 };
1542
1543 let json = serde_json::to_string(&metadata).unwrap();
1544 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1545 assert!(parsed.get("total_duration_ms").is_none());
1546 }
1547}