1use crate::agent::{AgentResult, AgentStatus, AgentType, EscalationInfo};
4use crate::quality::QualityReport;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Issue {
10 pub number: u64,
11 pub title: String,
12 pub body: String,
13 pub state: IssueStateGithub,
14 pub labels: Vec<String>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub assignee: Option<String>,
17 pub created_at: chrono::DateTime<chrono::Utc>,
18 pub updated_at: chrono::DateTime<chrono::Utc>,
19 pub url: String,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum IssueStateGithub {
26 Open,
27 Closed,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum IssueState {
34 Pending, Analyzing, Implementing, Reviewing, Deploying, Done, Blocked, Failed, }
43
44impl IssueState {
45 pub fn to_label(&self) -> &'static str {
47 match self {
48 IssueState::Pending => "📥 state:pending",
49 IssueState::Analyzing => "🔍 state:analyzing",
50 IssueState::Implementing => "🏗️ state:implementing",
51 IssueState::Reviewing => "👀 state:reviewing",
52 IssueState::Deploying => "🚀 state:deploying",
53 IssueState::Done => "✅ state:done",
54 IssueState::Blocked => "🚫 state:blocked",
55 IssueState::Failed => "❌ state:failed",
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct StateTransition {
63 pub from: IssueState,
64 pub to: IssueState,
65 pub timestamp: chrono::DateTime<chrono::Utc>,
66 pub triggered_by: String, #[serde(skip_serializing_if = "Option::is_none")]
68 pub reason: Option<String>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct AgentExecution {
74 pub agent_type: AgentType,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub task_id: Option<String>,
77 pub start_time: chrono::DateTime<chrono::Utc>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub duration_ms: Option<u64>,
82 pub status: AgentStatus,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub result: Option<AgentResult>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub error: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LabelChange {
92 pub timestamp: chrono::DateTime<chrono::Utc>,
93 pub action: LabelAction,
94 pub label: String,
95 pub performed_by: String, }
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum LabelAction {
101 Added,
102 Removed,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct TraceNote {
108 pub timestamp: chrono::DateTime<chrono::Utc>,
109 pub author: String, pub content: String,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub tags: Option<Vec<String>>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PRResult {
118 pub number: u64,
119 pub url: String,
120 pub state: PRState,
121 pub created_at: chrono::DateTime<chrono::Utc>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "lowercase")]
126pub enum PRState {
127 Draft,
128 Open,
129 Merged,
130 Closed,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct DeploymentResult {
136 pub environment: Environment,
137 pub version: String,
138 pub project_id: String,
139 pub deployment_url: String,
140 pub deployed_at: chrono::DateTime<chrono::Utc>,
141 pub duration_ms: u64,
142 pub status: DeploymentStatus,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum Environment {
148 Staging,
149 Production,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum DeploymentStatus {
155 Success,
156 Failed,
157 RolledBack,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "PascalCase")]
163pub enum ImpactLevel {
164 Critical,
165 High,
166 Medium,
167 Low,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct IssueAnalysis {
173 pub issue_number: u64,
174 pub issue_type: crate::task::TaskType,
175 pub severity: crate::agent::Severity,
176 pub impact: ImpactLevel,
177 pub assigned_agent: crate::agent::AgentType,
178 pub estimated_duration: u32, pub dependencies: Vec<String>,
180 pub labels: Vec<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct IssueTraceLog {
186 pub issue_number: u64,
188 pub issue_title: String,
189 pub issue_url: String,
190
191 pub created_at: chrono::DateTime<chrono::Utc>,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub closed_at: Option<chrono::DateTime<chrono::Utc>>,
195 pub current_state: IssueState,
196 pub state_transitions: Vec<StateTransition>,
197
198 pub agent_executions: Vec<AgentExecution>,
200
201 pub total_tasks: u32,
203 pub completed_tasks: u32,
204 pub failed_tasks: u32,
205
206 pub label_changes: Vec<LabelChange>,
208 pub current_labels: Vec<String>,
209
210 pub quality_reports: Vec<QualityReport>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub final_quality_score: Option<u8>,
214
215 pub pull_requests: Vec<PRResult>,
217
218 pub deployments: Vec<DeploymentResult>,
220
221 pub escalations: Vec<EscalationInfo>,
223
224 pub notes: Vec<TraceNote>,
226
227 pub metadata: IssueMetadata,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct IssueMetadata {
233 pub device_identifier: String,
234 pub session_ids: Vec<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub total_duration_ms: Option<u64>,
237 pub last_updated: chrono::DateTime<chrono::Utc>,
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
249 fn test_issue_state_github_serialization() {
250 let state = IssueStateGithub::Open;
251 let json = serde_json::to_string(&state).unwrap();
252 assert_eq!(json, "\"open\"");
253
254 let state = IssueStateGithub::Closed;
255 let json = serde_json::to_string(&state).unwrap();
256 assert_eq!(json, "\"closed\"");
257 }
258
259 #[test]
260 fn test_issue_state_github_roundtrip() {
261 let states = vec![IssueStateGithub::Open, IssueStateGithub::Closed];
262
263 for state in states {
264 let json = serde_json::to_string(&state).unwrap();
265 let deserialized: IssueStateGithub = serde_json::from_str(&json).unwrap();
266 assert_eq!(state, deserialized);
267 }
268 }
269
270 #[test]
275 fn test_issue_state_to_label() {
276 assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
277 assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
278 assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
279 assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
280 assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
281 assert_eq!(IssueState::Done.to_label(), "✅ state:done");
282 assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
283 assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
284 }
285
286 #[test]
287 fn test_issue_state_serialization() {
288 let state = IssueState::Pending;
289 let json = serde_json::to_string(&state).unwrap();
290 assert_eq!(json, "\"pending\"");
291
292 let state = IssueState::Done;
293 let json = serde_json::to_string(&state).unwrap();
294 assert_eq!(json, "\"done\"");
295 }
296
297 #[test]
298 fn test_issue_state_roundtrip() {
299 let states = vec![
300 IssueState::Pending,
301 IssueState::Analyzing,
302 IssueState::Implementing,
303 IssueState::Reviewing,
304 IssueState::Deploying,
305 IssueState::Done,
306 IssueState::Blocked,
307 IssueState::Failed,
308 ];
309
310 for state in states {
311 let json = serde_json::to_string(&state).unwrap();
312 let deserialized: IssueState = serde_json::from_str(&json).unwrap();
313 assert_eq!(state, deserialized);
314 }
315 }
316
317 #[test]
322 fn test_issue_serialization() {
323 let issue = Issue {
324 number: 123,
325 title: "Test issue".to_string(),
326 body: "Issue body".to_string(),
327 state: IssueStateGithub::Open,
328 labels: vec!["bug".to_string(), "priority:high".to_string()],
329 assignee: Some("user123".to_string()),
330 created_at: chrono::Utc::now(),
331 updated_at: chrono::Utc::now(),
332 url: "https://github.com/user/repo/issues/123".to_string(),
333 };
334
335 let json = serde_json::to_string(&issue).unwrap();
336 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
337 assert_eq!(parsed["number"], 123);
338 assert_eq!(parsed["title"], "Test issue");
339 assert_eq!(parsed["state"], "open");
340 assert_eq!(parsed["assignee"], "user123");
341 }
342
343 #[test]
344 fn test_issue_optional_assignee() {
345 let issue = Issue {
346 number: 456,
347 title: "Unassigned issue".to_string(),
348 body: "".to_string(),
349 state: IssueStateGithub::Closed,
350 labels: vec![],
351 assignee: None,
352 created_at: chrono::Utc::now(),
353 updated_at: chrono::Utc::now(),
354 url: "https://github.com/user/repo/issues/456".to_string(),
355 };
356
357 let json = serde_json::to_string(&issue).unwrap();
358 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
359 assert!(parsed.get("assignee").is_none());
360 }
361
362 #[test]
363 fn test_issue_roundtrip() {
364 let issue = Issue {
365 number: 789,
366 title: "Roundtrip test".to_string(),
367 body: "Test".to_string(),
368 state: IssueStateGithub::Open,
369 labels: vec!["test".to_string()],
370 assignee: Some("tester".to_string()),
371 created_at: chrono::Utc::now(),
372 updated_at: chrono::Utc::now(),
373 url: "https://github.com/user/repo/issues/789".to_string(),
374 };
375
376 let json = serde_json::to_string(&issue).unwrap();
377 let deserialized: Issue = serde_json::from_str(&json).unwrap();
378 assert_eq!(issue.number, deserialized.number);
379 assert_eq!(issue.title, deserialized.title);
380 assert_eq!(issue.state, deserialized.state);
381 }
382
383 #[test]
388 fn test_state_transition_serialization() {
389 let transition = StateTransition {
390 from: IssueState::Pending,
391 to: IssueState::Analyzing,
392 timestamp: chrono::Utc::now(),
393 triggered_by: "CoordinatorAgent".to_string(),
394 reason: Some("Starting analysis".to_string()),
395 };
396
397 let json = serde_json::to_string(&transition).unwrap();
398 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
399 assert_eq!(parsed["from"], "pending");
400 assert_eq!(parsed["to"], "analyzing");
401 assert_eq!(parsed["triggered_by"], "CoordinatorAgent");
402 }
403
404 #[test]
405 fn test_state_transition_roundtrip() {
406 let transition = StateTransition {
407 from: IssueState::Implementing,
408 to: IssueState::Reviewing,
409 timestamp: chrono::Utc::now(),
410 triggered_by: "ReviewAgent".to_string(),
411 reason: None,
412 };
413
414 let json = serde_json::to_string(&transition).unwrap();
415 let deserialized: StateTransition = serde_json::from_str(&json).unwrap();
416 assert_eq!(transition.from, deserialized.from);
417 assert_eq!(transition.to, deserialized.to);
418 }
419
420 #[test]
425 fn test_agent_execution_serialization() {
426 let execution = AgentExecution {
427 agent_type: AgentType::CodeGenAgent,
428 task_id: Some("task-001".to_string()),
429 start_time: chrono::Utc::now(),
430 end_time: Some(chrono::Utc::now()),
431 duration_ms: Some(5000),
432 status: AgentStatus::Completed,
433 result: None,
434 error: None,
435 };
436
437 let json = serde_json::to_string(&execution).unwrap();
438 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
439 assert_eq!(parsed["agent_type"], "CodeGenAgent");
440 assert_eq!(parsed["status"], "completed");
441 assert_eq!(parsed["duration_ms"], 5000);
442 }
443
444 #[test]
445 fn test_agent_execution_with_error() {
446 let execution = AgentExecution {
447 agent_type: AgentType::DeploymentAgent,
448 task_id: Some("task-002".to_string()),
449 start_time: chrono::Utc::now(),
450 end_time: Some(chrono::Utc::now()),
451 duration_ms: Some(1000),
452 status: AgentStatus::Failed,
453 result: None,
454 error: Some("Deployment failed".to_string()),
455 };
456
457 let json = serde_json::to_string(&execution).unwrap();
458 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
459 assert_eq!(parsed["error"], "Deployment failed");
460 }
461
462 #[test]
467 fn test_label_action_serialization() {
468 let action = LabelAction::Added;
469 let json = serde_json::to_string(&action).unwrap();
470 assert_eq!(json, "\"added\"");
471
472 let action = LabelAction::Removed;
473 let json = serde_json::to_string(&action).unwrap();
474 assert_eq!(json, "\"removed\"");
475 }
476
477 #[test]
478 fn test_label_action_roundtrip() {
479 let actions = vec![LabelAction::Added, LabelAction::Removed];
480
481 for action in actions {
482 let json = serde_json::to_string(&action).unwrap();
483 let deserialized: LabelAction = serde_json::from_str(&json).unwrap();
484 assert_eq!(action, deserialized);
485 }
486 }
487
488 #[test]
493 fn test_label_change_serialization() {
494 let change = LabelChange {
495 timestamp: chrono::Utc::now(),
496 action: LabelAction::Added,
497 label: "bug".to_string(),
498 performed_by: "IssueAgent".to_string(),
499 };
500
501 let json = serde_json::to_string(&change).unwrap();
502 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
503 assert_eq!(parsed["action"], "added");
504 assert_eq!(parsed["label"], "bug");
505 }
506
507 #[test]
512 fn test_pr_state_serialization() {
513 let state = PRState::Draft;
514 let json = serde_json::to_string(&state).unwrap();
515 assert_eq!(json, "\"draft\"");
516
517 let state = PRState::Merged;
518 let json = serde_json::to_string(&state).unwrap();
519 assert_eq!(json, "\"merged\"");
520 }
521
522 #[test]
523 fn test_pr_state_roundtrip() {
524 let states = vec![
525 PRState::Draft,
526 PRState::Open,
527 PRState::Merged,
528 PRState::Closed,
529 ];
530
531 for state in states {
532 let json = serde_json::to_string(&state).unwrap();
533 let deserialized: PRState = serde_json::from_str(&json).unwrap();
534 assert_eq!(state, deserialized);
535 }
536 }
537
538 #[test]
543 fn test_pr_result_serialization() {
544 let pr = PRResult {
545 number: 42,
546 url: "https://github.com/user/repo/pull/42".to_string(),
547 state: PRState::Open,
548 created_at: chrono::Utc::now(),
549 };
550
551 let json = serde_json::to_string(&pr).unwrap();
552 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
553 assert_eq!(parsed["number"], 42);
554 assert_eq!(parsed["state"], "open");
555 }
556
557 #[test]
562 fn test_environment_serialization() {
563 let env = Environment::Staging;
564 let json = serde_json::to_string(&env).unwrap();
565 assert_eq!(json, "\"staging\"");
566
567 let env = Environment::Production;
568 let json = serde_json::to_string(&env).unwrap();
569 assert_eq!(json, "\"production\"");
570 }
571
572 #[test]
577 fn test_deployment_status_serialization() {
578 let status = DeploymentStatus::Success;
579 let json = serde_json::to_string(&status).unwrap();
580 assert_eq!(json, "\"success\"");
581
582 let status = DeploymentStatus::RolledBack;
583 let json = serde_json::to_string(&status).unwrap();
584 assert_eq!(json, "\"rolled_back\"");
585 }
586
587 #[test]
588 fn test_deployment_status_roundtrip() {
589 let statuses = vec![
590 DeploymentStatus::Success,
591 DeploymentStatus::Failed,
592 DeploymentStatus::RolledBack,
593 ];
594
595 for status in statuses {
596 let json = serde_json::to_string(&status).unwrap();
597 let deserialized: DeploymentStatus = serde_json::from_str(&json).unwrap();
598 assert_eq!(status, deserialized);
599 }
600 }
601
602 #[test]
607 fn test_deployment_result_serialization() {
608 let deployment = DeploymentResult {
609 environment: Environment::Production,
610 version: "v1.2.3".to_string(),
611 project_id: "project-123".to_string(),
612 deployment_url: "https://app.example.com".to_string(),
613 deployed_at: chrono::Utc::now(),
614 duration_ms: 30000,
615 status: DeploymentStatus::Success,
616 };
617
618 let json = serde_json::to_string(&deployment).unwrap();
619 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
620 assert_eq!(parsed["environment"], "production");
621 assert_eq!(parsed["version"], "v1.2.3");
622 assert_eq!(parsed["status"], "success");
623 }
624
625 #[test]
630 fn test_trace_note_serialization() {
631 let note = TraceNote {
632 timestamp: chrono::Utc::now(),
633 author: "user123".to_string(),
634 content: "This is a note".to_string(),
635 tags: Some(vec!["important".to_string()]),
636 };
637
638 let json = serde_json::to_string(¬e).unwrap();
639 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
640 assert_eq!(parsed["content"], "This is a note");
641 assert_eq!(parsed["tags"][0], "important");
642 }
643
644 #[test]
649 fn test_issue_metadata_serialization() {
650 let metadata = IssueMetadata {
651 device_identifier: "MacBook-Pro".to_string(),
652 session_ids: vec!["session-1".to_string(), "session-2".to_string()],
653 total_duration_ms: Some(120000),
654 last_updated: chrono::Utc::now(),
655 };
656
657 let json = serde_json::to_string(&metadata).unwrap();
658 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
659 assert_eq!(parsed["device_identifier"], "MacBook-Pro");
660 assert_eq!(parsed["total_duration_ms"], 120000);
661 }
662
663 #[test]
668 fn test_issue_trace_log_structure() {
669 let metadata = IssueMetadata {
670 device_identifier: "test-device".to_string(),
671 session_ids: vec!["session-1".to_string()],
672 total_duration_ms: Some(60000),
673 last_updated: chrono::Utc::now(),
674 };
675
676 let trace_log = IssueTraceLog {
677 issue_number: 100,
678 issue_title: "Test issue".to_string(),
679 issue_url: "https://github.com/user/repo/issues/100".to_string(),
680 created_at: chrono::Utc::now(),
681 closed_at: None,
682 current_state: IssueState::Implementing,
683 state_transitions: vec![],
684 agent_executions: vec![],
685 total_tasks: 5,
686 completed_tasks: 2,
687 failed_tasks: 0,
688 label_changes: vec![],
689 current_labels: vec!["bug".to_string()],
690 quality_reports: vec![],
691 final_quality_score: None,
692 pull_requests: vec![],
693 deployments: vec![],
694 escalations: vec![],
695 notes: vec![],
696 metadata,
697 };
698
699 let json = serde_json::to_string(&trace_log).unwrap();
700 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
701 assert_eq!(parsed["issue_number"], 100);
702 assert_eq!(parsed["current_state"], "implementing");
703 assert_eq!(parsed["total_tasks"], 5);
704 assert_eq!(parsed["completed_tasks"], 2);
705 }
706}