miyabi_types/
issue.rs

1//! Issue and trace logging types
2
3use crate::agent::{AgentResult, AgentStatus, AgentType, EscalationInfo, ImpactLevel};
4use crate::quality::QualityReport;
5use serde::{Deserialize, Serialize};
6
7/// GitHub Issue
8///
9/// Represents a GitHub issue with its metadata, state, and lifecycle information.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Issue {
12    /// Issue number (starts at 1 in GitHub)
13    pub number: u64,
14    /// Issue title (max 256 characters recommended)
15    pub title: String,
16    /// Issue body/description in Markdown format
17    pub body: String,
18    /// GitHub state (open/closed)
19    pub state: IssueStateGithub,
20    /// List of label names attached to this issue
21    pub labels: Vec<String>,
22    /// Optional GitHub username of assigned user
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub assignee: Option<String>,
25    /// Timestamp when the issue was created
26    pub created_at: chrono::DateTime<chrono::Utc>,
27    /// Timestamp when the issue was last updated
28    pub updated_at: chrono::DateTime<chrono::Utc>,
29    /// Full URL to the issue (e.g., "https://github.com/owner/repo/issues/123")
30    pub url: String,
31}
32
33impl Issue {
34    /// Validate issue fields
35    ///
36    /// # Returns
37    /// * `Ok(())` if all validations pass
38    /// * `Err(String)` with detailed error message if validation fails
39    ///
40    /// # Examples
41    /// ```
42    /// use miyabi_types::issue::{Issue, IssueStateGithub};
43    ///
44    /// let issue = Issue {
45    ///     number: 123,
46    ///     title: "Valid issue".to_string(),
47    ///     body: "Description".to_string(),
48    ///     state: IssueStateGithub::Open,
49    ///     labels: vec![],
50    ///     assignee: None,
51    ///     created_at: chrono::Utc::now(),
52    ///     updated_at: chrono::Utc::now(),
53    ///     url: "https://github.com/owner/repo/issues/123".to_string(),
54    /// };
55    ///
56    /// assert!(issue.validate().is_ok());
57    /// ```
58    pub fn validate(&self) -> Result<(), String> {
59        // Validate number
60        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        // Validate title
67        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        // Validate URL format
82        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        // Validate URL contains issue number
91        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        // Validate timestamps
100        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/// GitHub issue state (open/closed)
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "lowercase")]
115pub enum IssueStateGithub {
116    /// Issue is open and active
117    Open,
118    /// Issue is closed and resolved
119    Closed,
120}
121
122/// Issue state in Miyabi lifecycle (8 states)
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "lowercase")]
125pub enum IssueState {
126    /// Issue created, awaiting triage by CoordinatorAgent
127    Pending,
128    /// CoordinatorAgent is analyzing and decomposing the issue into tasks
129    Analyzing,
130    /// Specialist Agents (CodeGen, Review, etc.) are working on tasks
131    Implementing,
132    /// ReviewAgent is checking code quality and running tests
133    Reviewing,
134    /// DeploymentAgent is deploying changes to staging/production
135    Deploying,
136    /// Issue completed successfully with all tasks done
137    Done,
138    /// Issue is blocked and requires human intervention
139    Blocked,
140    /// Execution failed - needs manual review and restart
141    Failed,
142}
143
144impl IssueState {
145    /// Get corresponding GitHub label for this state
146    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/// State transition record
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct StateTransition {
163    /// Previous state before transition
164    pub from: IssueState,
165    /// New state after transition
166    pub to: IssueState,
167    /// Timestamp when transition occurred
168    pub timestamp: chrono::DateTime<chrono::Utc>,
169    /// Who triggered the transition (Agent name or "user")
170    pub triggered_by: String,
171    /// Optional reason for the transition
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub reason: Option<String>,
174}
175
176/// Agent execution record
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct AgentExecution {
179    /// Type of agent that executed (CodeGenAgent, ReviewAgent, etc.)
180    pub agent_type: AgentType,
181    /// Optional task ID if this execution was for a specific task
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub task_id: Option<String>,
184    /// Timestamp when execution started
185    pub start_time: chrono::DateTime<chrono::Utc>,
186    /// Timestamp when execution ended (None if still running)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
189    /// Execution duration in milliseconds (None if still running)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub duration_ms: Option<u64>,
192    /// Current status of the execution
193    pub status: AgentStatus,
194    /// Execution result (None if failed or still running)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub result: Option<AgentResult>,
197    /// Error message if execution failed
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub error: Option<String>,
200}
201
202/// Label change record
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct LabelChange {
205    /// Timestamp when label was changed
206    pub timestamp: chrono::DateTime<chrono::Utc>,
207    /// Action performed (added or removed)
208    pub action: LabelAction,
209    /// Label name that was added or removed
210    pub label: String,
211    /// Who performed the action (Agent name or "user")
212    pub performed_by: String,
213}
214
215/// Label action type
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum LabelAction {
219    /// Label was added to the issue
220    Added,
221    /// Label was removed from the issue
222    Removed,
223}
224
225/// Trace note (manual annotation)
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct TraceNote {
228    /// Timestamp when note was created
229    pub timestamp: chrono::DateTime<chrono::Utc>,
230    /// Author of the note (Agent name or "user")
231    pub author: String,
232    /// Note content in Markdown format
233    pub content: String,
234    /// Optional tags for categorization
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub tags: Option<Vec<String>>,
237}
238
239/// Pull Request result
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct PRResult {
242    /// Pull request number
243    pub number: u64,
244    /// Full URL to the pull request
245    pub url: String,
246    /// Current state of the pull request
247    pub state: PRState,
248    /// Timestamp when PR was created
249    pub created_at: chrono::DateTime<chrono::Utc>,
250}
251
252/// Pull request state
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "lowercase")]
255pub enum PRState {
256    /// Draft PR (not ready for review)
257    Draft,
258    /// Open PR (ready for review)
259    Open,
260    /// PR has been merged
261    Merged,
262    /// PR was closed without merging
263    Closed,
264}
265
266/// Deployment result
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct DeploymentResult {
269    /// Target environment (staging or production)
270    pub environment: Environment,
271    /// Deployed version string (e.g., "v1.2.3" or git commit hash)
272    pub version: String,
273    /// Project ID in the deployment platform (Firebase, Vercel, etc.)
274    pub project_id: String,
275    /// URL where the deployment is accessible
276    pub deployment_url: String,
277    /// Timestamp when deployment completed
278    pub deployed_at: chrono::DateTime<chrono::Utc>,
279    /// Deployment duration in milliseconds
280    pub duration_ms: u64,
281    /// Final deployment status
282    pub status: DeploymentStatus,
283}
284
285/// Deployment environment
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "lowercase")]
288pub enum Environment {
289    /// Staging environment for testing
290    Staging,
291    /// Production environment for end users
292    Production,
293}
294
295/// Deployment status
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(rename_all = "snake_case")]
298pub enum DeploymentStatus {
299    /// Deployment completed successfully
300    Success,
301    /// Deployment failed
302    Failed,
303    /// Deployment was rolled back to previous version
304    RolledBack,
305}
306
307/// Issue analysis result from IssueAgent
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct IssueAnalysis {
310    /// Issue number being analyzed
311    pub issue_number: u64,
312    /// Inferred issue type (feature, bug, refactor, etc.)
313    pub issue_type: crate::task::TaskType,
314    /// Severity level (Sev.1-Critical to Sev.4-Low)
315    pub severity: crate::agent::Severity,
316    /// Impact level (high, medium, low)
317    pub impact: ImpactLevel,
318    /// Agent recommended to handle this issue
319    pub assigned_agent: crate::agent::AgentType,
320    /// Estimated duration in minutes
321    pub estimated_duration: u32,
322    /// List of dependency issue numbers (e.g., ["#270", "#271"])
323    pub dependencies: Vec<String>,
324    /// Recommended labels to apply
325    pub labels: Vec<String>,
326}
327
328/// Issue Trace Log - complete lifecycle tracking
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct IssueTraceLog {
331    // Identification
332    /// Issue number
333    pub issue_number: u64,
334    /// Issue title
335    pub issue_title: String,
336    /// Full URL to the issue
337    pub issue_url: String,
338
339    // Lifecycle tracking
340    /// Timestamp when issue was created
341    pub created_at: chrono::DateTime<chrono::Utc>,
342    /// Timestamp when issue was closed (None if still open)
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub closed_at: Option<chrono::DateTime<chrono::Utc>>,
345    /// Current state of the issue
346    pub current_state: IssueState,
347    /// History of all state transitions
348    pub state_transitions: Vec<StateTransition>,
349
350    // Agent execution tracking
351    /// List of all agent executions on this issue
352    pub agent_executions: Vec<AgentExecution>,
353
354    // Task decomposition
355    /// Total number of tasks decomposed from this issue
356    pub total_tasks: u32,
357    /// Number of tasks completed successfully
358    pub completed_tasks: u32,
359    /// Number of tasks that failed
360    pub failed_tasks: u32,
361
362    // Label tracking
363    /// History of all label changes
364    pub label_changes: Vec<LabelChange>,
365    /// Current labels applied to the issue
366    pub current_labels: Vec<String>,
367
368    // Quality & metrics
369    /// List of quality reports from ReviewAgent
370    pub quality_reports: Vec<QualityReport>,
371    /// Final quality score (0-100) if available
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub final_quality_score: Option<u8>,
374
375    // Pull Request tracking
376    /// List of pull requests created for this issue
377    pub pull_requests: Vec<PRResult>,
378
379    // Deployment tracking
380    /// List of deployments triggered by this issue
381    pub deployments: Vec<DeploymentResult>,
382
383    // Escalations
384    /// List of escalations requiring human intervention
385    pub escalations: Vec<EscalationInfo>,
386
387    // Notes & annotations
388    /// Manual notes and annotations from agents or users
389    pub notes: Vec<TraceNote>,
390
391    // Metadata
392    /// Additional metadata for the issue
393    pub metadata: IssueMetadata,
394}
395
396impl IssueTraceLog {
397    /// Validate issue trace log fields
398    ///
399    /// # Returns
400    /// * `Ok(())` if all validations pass
401    /// * `Err(String)` with detailed error message if validation fails
402    pub fn validate(&self) -> Result<(), String> {
403        // Validate issue number
404        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        // Validate title
411        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        // Validate URL format
418        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        // Validate timestamps
427        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        // Validate task counts consistency
438        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        // Validate final quality score range
448        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/// Issue metadata
463#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct IssueMetadata {
465    /// Device identifier where the issue was processed (e.g., "MacBook", "Server01")
466    pub device_identifier: String,
467    /// List of session IDs that worked on this issue
468    pub session_ids: Vec<String>,
469    /// Total duration spent on this issue in milliseconds
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub total_duration_ms: Option<u64>,
472    /// Timestamp when the metadata was last updated
473    pub last_updated: chrono::DateTime<chrono::Utc>,
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    // ========================================================================
481    // IssueStateGithub Tests
482    // ========================================================================
483
484    #[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    // ========================================================================
507    // IssueState Tests
508    // ========================================================================
509
510    #[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    // ========================================================================
554    // Issue Tests
555    // ========================================================================
556
557    #[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    // ========================================================================
620    // StateTransition Tests
621    // ========================================================================
622
623    #[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    // ========================================================================
657    // AgentExecution Tests
658    // ========================================================================
659
660    #[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    // ========================================================================
699    // LabelAction Tests
700    // ========================================================================
701
702    #[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    // ========================================================================
725    // LabelChange Tests
726    // ========================================================================
727
728    #[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    // ========================================================================
744    // PRState Tests
745    // ========================================================================
746
747    #[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    // ========================================================================
775    // PRResult Tests
776    // ========================================================================
777
778    #[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    // ========================================================================
794    // Environment Tests
795    // ========================================================================
796
797    #[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    // ========================================================================
809    // DeploymentStatus Tests
810    // ========================================================================
811
812    #[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    // ========================================================================
839    // DeploymentResult Tests
840    // ========================================================================
841
842    #[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    // ========================================================================
862    // TraceNote Tests
863    // ========================================================================
864
865    #[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(&note).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    // ========================================================================
881    // IssueMetadata Tests
882    // ========================================================================
883
884    #[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    // ========================================================================
900    // IssueTraceLog Tests
901    // ========================================================================
902
903    #[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    // ========================================================================
944    // Issue::validate() Tests
945    // ========================================================================
946
947    #[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    // ========================================================================
1102    // IssueTraceLog::validate() Tests
1103    // ========================================================================
1104
1105    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    // ========================================================================
1355    // Additional Struct Tests
1356    // ========================================================================
1357
1358    #[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(&note).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(&note).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}