1use oxios_gateway::format::ChannelFormatter;
7use oxios_gateway::message::{ErrorKind, OutgoingMessage};
8
9pub struct CliFormatter;
14
15impl ChannelFormatter for CliFormatter {
16 fn format_success(&self, msg: &OutgoingMessage) -> String {
17 let mut out = msg.content.clone();
18
19 if let Some(meta) = &msg.meta {
20 let eval_icon = if meta.evaluation_passed {
21 "✅"
22 } else {
23 "⚠️"
24 };
25 if !meta.phase.is_empty() {
26 out.push_str(&format!(
27 "\n{} {} | {}",
28 eval_icon,
29 meta.phase,
30 if meta.evaluation_passed {
31 "통과"
32 } else {
33 "미통과"
34 }
35 ));
36 }
37
38 if let Some(tag) = &meta.project_tag {
39 out.push_str(&format!(" | {tag}"));
40 }
41
42 if let Some(dur) = meta.duration_ms {
43 if dur >= 1000 {
44 out.push_str(&format!(" | {:.1}s", dur as f64 / 1000.0));
45 } else {
46 out.push_str(&format!(" | {dur}ms"));
47 }
48 }
49 }
50
51 out
52 }
53
54 fn format_error(&self, msg: &OutgoingMessage) -> String {
55 let meta = msg.meta.as_ref();
56 let kind = meta.and_then(|m| m.error.as_ref()).map(|e| e.kind);
57
58 let icon = match kind {
59 Some(ErrorKind::ExecutionFailed) => "❌",
60 Some(ErrorKind::ProviderError) => "🔌",
61 Some(ErrorKind::Timeout) => "⏱️",
62 Some(ErrorKind::PermissionDenied) => "🔒",
63 Some(ErrorKind::ValidationError) => "⚠️",
64 _ => "💥",
65 };
66
67 let mut out = format!("{} {}", icon, msg.content);
68
69 if let Some(err) = meta.and_then(|m| m.error.as_ref()) {
70 if let Some(s) = &err.suggestion {
71 out.push_str(&format!("\n💡 {s}"));
72 }
73 }
74
75 out
76 }
77
78 fn format_progress(&self, phase: &str) -> String {
79 match phase {
80 "Interview" => "🔍 분석 중...".into(),
81 "Seed" => "📋 계획 수립 중...".into(),
82 "Execute" => "⚡ 실행 중...".into(),
83 "Evaluate" => "📊 평가 중...".into(),
84 "Evolve" => "🔄 개선 중...".into(),
85 _ => "⏳ 처리 중...".into(),
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use oxios_gateway::message::{ResponseMeta, UserFacingError};
94 use std::collections::HashMap;
95
96 fn make_msg(content: &str, meta: Option<ResponseMeta>) -> OutgoingMessage {
97 OutgoingMessage {
98 id: uuid::Uuid::new_v4(),
99 channel: "cli".into(),
100 user_id: "test-user".into(),
101 content: content.into(),
102 timestamp: chrono::Utc::now(),
103 metadata: HashMap::new(),
104 meta,
105 }
106 }
107
108 #[test]
109 fn format_success_no_meta() {
110 let msg = make_msg("Hello", None);
111 let formatter = CliFormatter;
112 assert_eq!(formatter.format_success(&msg), "Hello");
113 }
114
115 #[test]
116 fn format_success_with_phase_and_eval() {
117 let meta = ResponseMeta {
118 session_id: None,
119 project_id: None,
120 project_tag: Some("[🔧 oxios]".into()),
121 seed_id: None,
122 phase: "Execute".into(),
123 evaluation_passed: true,
124 duration_ms: Some(1500),
125 error: None,
126 };
127 let msg = make_msg("Done!", Some(meta));
128 let formatter = CliFormatter;
129 let output = formatter.format_success(&msg);
130 assert!(output.contains("✅ Execute | 통과"));
131 assert!(output.contains("[🔧 oxios]"));
132 assert!(output.contains("1.5s"));
133 }
134
135 #[test]
136 fn format_success_failed_eval() {
137 let meta = ResponseMeta {
138 session_id: None,
139 project_id: None,
140 project_tag: None,
141 seed_id: None,
142 phase: "Evaluate".into(),
143 evaluation_passed: false,
144 duration_ms: Some(500),
145 error: None,
146 };
147 let msg = make_msg("Partial", Some(meta));
148 let formatter = CliFormatter;
149 let output = formatter.format_success(&msg);
150 assert!(output.contains("⚠️ Evaluate | 미통과"));
151 assert!(output.contains("500ms"));
152 }
153
154 #[test]
155 fn format_error_timeout() {
156 let meta = ResponseMeta {
157 session_id: None,
158 project_id: None,
159 project_tag: None,
160 seed_id: None,
161 phase: String::new(),
162 evaluation_passed: false,
163 duration_ms: None,
164 error: Some(UserFacingError {
165 message: "시간이 초과되었습니다.".into(),
166 kind: ErrorKind::Timeout,
167 suggestion: Some("더 간단한 요청으로 시도하세요.".into()),
168 }),
169 };
170 let msg = make_msg("시간이 초과되었습니다.", Some(meta));
171 let formatter = CliFormatter;
172 let output = formatter.format_error(&msg);
173 assert!(output.starts_with("⏱️"));
174 assert!(output.contains("💡 더 간단한 요청으로 시도하세요."));
175 }
176
177 #[test]
178 fn format_error_provider() {
179 let meta = ResponseMeta {
180 session_id: None,
181 project_id: None,
182 project_tag: None,
183 seed_id: None,
184 phase: String::new(),
185 evaluation_passed: false,
186 duration_ms: None,
187 error: Some(UserFacingError {
188 message: "AI 서비스 오류.".into(),
189 kind: ErrorKind::ProviderError,
190 suggestion: None,
191 }),
192 };
193 let msg = make_msg("AI 서비스 오류.", Some(meta));
194 let formatter = CliFormatter;
195 let output = formatter.format_error(&msg);
196 assert!(output.starts_with("🔌"));
197 assert!(!output.contains("💡")); }
199
200 #[test]
201 fn format_progress_phases() {
202 let formatter = CliFormatter;
203 assert_eq!(formatter.format_progress("Interview"), "🔍 분석 중...");
204 assert_eq!(formatter.format_progress("Seed"), "📋 계획 수립 중...");
205 assert_eq!(formatter.format_progress("Execute"), "⚡ 실행 중...");
206 assert_eq!(formatter.format_progress("Evaluate"), "📊 평가 중...");
207 assert_eq!(formatter.format_progress("Evolve"), "🔄 개선 중...");
208 assert_eq!(formatter.format_progress("Unknown"), "⏳ 처리 중...");
209 }
210}