1use crate::agent::{AgentResult, AgentStatus, AgentType, EscalationInfo, ImpactLevel};
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, Serialize, Deserialize)]
162pub struct IssueAnalysis {
163 pub issue_number: u64,
164 pub issue_type: crate::task::TaskType,
165 pub severity: crate::agent::Severity,
166 pub impact: ImpactLevel,
167 pub assigned_agent: crate::agent::AgentType,
168 pub estimated_duration: u32, pub dependencies: Vec<String>,
170 pub labels: Vec<String>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct IssueTraceLog {
176 pub issue_number: u64,
178 pub issue_title: String,
179 pub issue_url: String,
180
181 pub created_at: chrono::DateTime<chrono::Utc>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub closed_at: Option<chrono::DateTime<chrono::Utc>>,
185 pub current_state: IssueState,
186 pub state_transitions: Vec<StateTransition>,
187
188 pub agent_executions: Vec<AgentExecution>,
190
191 pub total_tasks: u32,
193 pub completed_tasks: u32,
194 pub failed_tasks: u32,
195
196 pub label_changes: Vec<LabelChange>,
198 pub current_labels: Vec<String>,
199
200 pub quality_reports: Vec<QualityReport>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub final_quality_score: Option<u8>,
204
205 pub pull_requests: Vec<PRResult>,
207
208 pub deployments: Vec<DeploymentResult>,
210
211 pub escalations: Vec<EscalationInfo>,
213
214 pub notes: Vec<TraceNote>,
216
217 pub metadata: IssueMetadata,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct IssueMetadata {
223 pub device_identifier: String,
224 pub session_ids: Vec<String>,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub total_duration_ms: Option<u64>,
227 pub last_updated: chrono::DateTime<chrono::Utc>,
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
239 fn test_issue_state_github_serialization() {
240 let state = IssueStateGithub::Open;
241 let json = serde_json::to_string(&state).unwrap();
242 assert_eq!(json, "\"open\"");
243
244 let state = IssueStateGithub::Closed;
245 let json = serde_json::to_string(&state).unwrap();
246 assert_eq!(json, "\"closed\"");
247 }
248
249 #[test]
250 fn test_issue_state_github_roundtrip() {
251 let states = vec![IssueStateGithub::Open, IssueStateGithub::Closed];
252
253 for state in states {
254 let json = serde_json::to_string(&state).unwrap();
255 let deserialized: IssueStateGithub = serde_json::from_str(&json).unwrap();
256 assert_eq!(state, deserialized);
257 }
258 }
259
260 #[test]
265 fn test_issue_state_to_label() {
266 assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
267 assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
268 assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
269 assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
270 assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
271 assert_eq!(IssueState::Done.to_label(), "✅ state:done");
272 assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
273 assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
274 }
275
276 #[test]
277 fn test_issue_state_serialization() {
278 let state = IssueState::Pending;
279 let json = serde_json::to_string(&state).unwrap();
280 assert_eq!(json, "\"pending\"");
281
282 let state = IssueState::Done;
283 let json = serde_json::to_string(&state).unwrap();
284 assert_eq!(json, "\"done\"");
285 }
286
287 #[test]
288 fn test_issue_state_roundtrip() {
289 let states = vec![
290 IssueState::Pending,
291 IssueState::Analyzing,
292 IssueState::Implementing,
293 IssueState::Reviewing,
294 IssueState::Deploying,
295 IssueState::Done,
296 IssueState::Blocked,
297 IssueState::Failed,
298 ];
299
300 for state in states {
301 let json = serde_json::to_string(&state).unwrap();
302 let deserialized: IssueState = serde_json::from_str(&json).unwrap();
303 assert_eq!(state, deserialized);
304 }
305 }
306
307 #[test]
312 fn test_issue_serialization() {
313 let issue = Issue {
314 number: 123,
315 title: "Test issue".to_string(),
316 body: "Issue body".to_string(),
317 state: IssueStateGithub::Open,
318 labels: vec!["bug".to_string(), "priority:high".to_string()],
319 assignee: Some("user123".to_string()),
320 created_at: chrono::Utc::now(),
321 updated_at: chrono::Utc::now(),
322 url: "https://github.com/user/repo/issues/123".to_string(),
323 };
324
325 let json = serde_json::to_string(&issue).unwrap();
326 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
327 assert_eq!(parsed["number"], 123);
328 assert_eq!(parsed["title"], "Test issue");
329 assert_eq!(parsed["state"], "open");
330 assert_eq!(parsed["assignee"], "user123");
331 }
332
333 #[test]
334 fn test_issue_optional_assignee() {
335 let issue = Issue {
336 number: 456,
337 title: "Unassigned issue".to_string(),
338 body: "".to_string(),
339 state: IssueStateGithub::Closed,
340 labels: vec![],
341 assignee: None,
342 created_at: chrono::Utc::now(),
343 updated_at: chrono::Utc::now(),
344 url: "https://github.com/user/repo/issues/456".to_string(),
345 };
346
347 let json = serde_json::to_string(&issue).unwrap();
348 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
349 assert!(parsed.get("assignee").is_none());
350 }
351
352 #[test]
353 fn test_issue_roundtrip() {
354 let issue = Issue {
355 number: 789,
356 title: "Roundtrip test".to_string(),
357 body: "Test".to_string(),
358 state: IssueStateGithub::Open,
359 labels: vec!["test".to_string()],
360 assignee: Some("tester".to_string()),
361 created_at: chrono::Utc::now(),
362 updated_at: chrono::Utc::now(),
363 url: "https://github.com/user/repo/issues/789".to_string(),
364 };
365
366 let json = serde_json::to_string(&issue).unwrap();
367 let deserialized: Issue = serde_json::from_str(&json).unwrap();
368 assert_eq!(issue.number, deserialized.number);
369 assert_eq!(issue.title, deserialized.title);
370 assert_eq!(issue.state, deserialized.state);
371 }
372
373 #[test]
378 fn test_state_transition_serialization() {
379 let transition = StateTransition {
380 from: IssueState::Pending,
381 to: IssueState::Analyzing,
382 timestamp: chrono::Utc::now(),
383 triggered_by: "CoordinatorAgent".to_string(),
384 reason: Some("Starting analysis".to_string()),
385 };
386
387 let json = serde_json::to_string(&transition).unwrap();
388 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
389 assert_eq!(parsed["from"], "pending");
390 assert_eq!(parsed["to"], "analyzing");
391 assert_eq!(parsed["triggered_by"], "CoordinatorAgent");
392 }
393
394 #[test]
395 fn test_state_transition_roundtrip() {
396 let transition = StateTransition {
397 from: IssueState::Implementing,
398 to: IssueState::Reviewing,
399 timestamp: chrono::Utc::now(),
400 triggered_by: "ReviewAgent".to_string(),
401 reason: None,
402 };
403
404 let json = serde_json::to_string(&transition).unwrap();
405 let deserialized: StateTransition = serde_json::from_str(&json).unwrap();
406 assert_eq!(transition.from, deserialized.from);
407 assert_eq!(transition.to, deserialized.to);
408 }
409
410 #[test]
415 fn test_agent_execution_serialization() {
416 let execution = AgentExecution {
417 agent_type: AgentType::CodeGenAgent,
418 task_id: Some("task-001".to_string()),
419 start_time: chrono::Utc::now(),
420 end_time: Some(chrono::Utc::now()),
421 duration_ms: Some(5000),
422 status: AgentStatus::Completed,
423 result: None,
424 error: None,
425 };
426
427 let json = serde_json::to_string(&execution).unwrap();
428 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
429 assert_eq!(parsed["agent_type"], "CodeGenAgent");
430 assert_eq!(parsed["status"], "completed");
431 assert_eq!(parsed["duration_ms"], 5000);
432 }
433
434 #[test]
435 fn test_agent_execution_with_error() {
436 let execution = AgentExecution {
437 agent_type: AgentType::DeploymentAgent,
438 task_id: Some("task-002".to_string()),
439 start_time: chrono::Utc::now(),
440 end_time: Some(chrono::Utc::now()),
441 duration_ms: Some(1000),
442 status: AgentStatus::Failed,
443 result: None,
444 error: Some("Deployment failed".to_string()),
445 };
446
447 let json = serde_json::to_string(&execution).unwrap();
448 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
449 assert_eq!(parsed["error"], "Deployment failed");
450 }
451
452 #[test]
457 fn test_label_action_serialization() {
458 let action = LabelAction::Added;
459 let json = serde_json::to_string(&action).unwrap();
460 assert_eq!(json, "\"added\"");
461
462 let action = LabelAction::Removed;
463 let json = serde_json::to_string(&action).unwrap();
464 assert_eq!(json, "\"removed\"");
465 }
466
467 #[test]
468 fn test_label_action_roundtrip() {
469 let actions = vec![LabelAction::Added, LabelAction::Removed];
470
471 for action in actions {
472 let json = serde_json::to_string(&action).unwrap();
473 let deserialized: LabelAction = serde_json::from_str(&json).unwrap();
474 assert_eq!(action, deserialized);
475 }
476 }
477
478 #[test]
483 fn test_label_change_serialization() {
484 let change = LabelChange {
485 timestamp: chrono::Utc::now(),
486 action: LabelAction::Added,
487 label: "bug".to_string(),
488 performed_by: "IssueAgent".to_string(),
489 };
490
491 let json = serde_json::to_string(&change).unwrap();
492 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
493 assert_eq!(parsed["action"], "added");
494 assert_eq!(parsed["label"], "bug");
495 }
496
497 #[test]
502 fn test_pr_state_serialization() {
503 let state = PRState::Draft;
504 let json = serde_json::to_string(&state).unwrap();
505 assert_eq!(json, "\"draft\"");
506
507 let state = PRState::Merged;
508 let json = serde_json::to_string(&state).unwrap();
509 assert_eq!(json, "\"merged\"");
510 }
511
512 #[test]
513 fn test_pr_state_roundtrip() {
514 let states = vec![
515 PRState::Draft,
516 PRState::Open,
517 PRState::Merged,
518 PRState::Closed,
519 ];
520
521 for state in states {
522 let json = serde_json::to_string(&state).unwrap();
523 let deserialized: PRState = serde_json::from_str(&json).unwrap();
524 assert_eq!(state, deserialized);
525 }
526 }
527
528 #[test]
533 fn test_pr_result_serialization() {
534 let pr = PRResult {
535 number: 42,
536 url: "https://github.com/user/repo/pull/42".to_string(),
537 state: PRState::Open,
538 created_at: chrono::Utc::now(),
539 };
540
541 let json = serde_json::to_string(&pr).unwrap();
542 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
543 assert_eq!(parsed["number"], 42);
544 assert_eq!(parsed["state"], "open");
545 }
546
547 #[test]
552 fn test_environment_serialization() {
553 let env = Environment::Staging;
554 let json = serde_json::to_string(&env).unwrap();
555 assert_eq!(json, "\"staging\"");
556
557 let env = Environment::Production;
558 let json = serde_json::to_string(&env).unwrap();
559 assert_eq!(json, "\"production\"");
560 }
561
562 #[test]
567 fn test_deployment_status_serialization() {
568 let status = DeploymentStatus::Success;
569 let json = serde_json::to_string(&status).unwrap();
570 assert_eq!(json, "\"success\"");
571
572 let status = DeploymentStatus::RolledBack;
573 let json = serde_json::to_string(&status).unwrap();
574 assert_eq!(json, "\"rolled_back\"");
575 }
576
577 #[test]
578 fn test_deployment_status_roundtrip() {
579 let statuses = vec![
580 DeploymentStatus::Success,
581 DeploymentStatus::Failed,
582 DeploymentStatus::RolledBack,
583 ];
584
585 for status in statuses {
586 let json = serde_json::to_string(&status).unwrap();
587 let deserialized: DeploymentStatus = serde_json::from_str(&json).unwrap();
588 assert_eq!(status, deserialized);
589 }
590 }
591
592 #[test]
597 fn test_deployment_result_serialization() {
598 let deployment = DeploymentResult {
599 environment: Environment::Production,
600 version: "v1.2.3".to_string(),
601 project_id: "project-123".to_string(),
602 deployment_url: "https://app.example.com".to_string(),
603 deployed_at: chrono::Utc::now(),
604 duration_ms: 30000,
605 status: DeploymentStatus::Success,
606 };
607
608 let json = serde_json::to_string(&deployment).unwrap();
609 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
610 assert_eq!(parsed["environment"], "production");
611 assert_eq!(parsed["version"], "v1.2.3");
612 assert_eq!(parsed["status"], "success");
613 }
614
615 #[test]
620 fn test_trace_note_serialization() {
621 let note = TraceNote {
622 timestamp: chrono::Utc::now(),
623 author: "user123".to_string(),
624 content: "This is a note".to_string(),
625 tags: Some(vec!["important".to_string()]),
626 };
627
628 let json = serde_json::to_string(¬e).unwrap();
629 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
630 assert_eq!(parsed["content"], "This is a note");
631 assert_eq!(parsed["tags"][0], "important");
632 }
633
634 #[test]
639 fn test_issue_metadata_serialization() {
640 let metadata = IssueMetadata {
641 device_identifier: "MacBook-Pro".to_string(),
642 session_ids: vec!["session-1".to_string(), "session-2".to_string()],
643 total_duration_ms: Some(120000),
644 last_updated: chrono::Utc::now(),
645 };
646
647 let json = serde_json::to_string(&metadata).unwrap();
648 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
649 assert_eq!(parsed["device_identifier"], "MacBook-Pro");
650 assert_eq!(parsed["total_duration_ms"], 120000);
651 }
652
653 #[test]
658 fn test_issue_trace_log_structure() {
659 let metadata = IssueMetadata {
660 device_identifier: "test-device".to_string(),
661 session_ids: vec!["session-1".to_string()],
662 total_duration_ms: Some(60000),
663 last_updated: chrono::Utc::now(),
664 };
665
666 let trace_log = IssueTraceLog {
667 issue_number: 100,
668 issue_title: "Test issue".to_string(),
669 issue_url: "https://github.com/user/repo/issues/100".to_string(),
670 created_at: chrono::Utc::now(),
671 closed_at: None,
672 current_state: IssueState::Implementing,
673 state_transitions: vec![],
674 agent_executions: vec![],
675 total_tasks: 5,
676 completed_tasks: 2,
677 failed_tasks: 0,
678 label_changes: vec![],
679 current_labels: vec!["bug".to_string()],
680 quality_reports: vec![],
681 final_quality_score: None,
682 pull_requests: vec![],
683 deployments: vec![],
684 escalations: vec![],
685 notes: vec![],
686 metadata,
687 };
688
689 let json = serde_json::to_string(&trace_log).unwrap();
690 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
691 assert_eq!(parsed["issue_number"], 100);
692 assert_eq!(parsed["current_state"], "implementing");
693 assert_eq!(parsed["total_tasks"], 5);
694 assert_eq!(parsed["completed_tasks"], 2);
695 }
696}