1use std::io;
7
8use crate::ipc::error_codes::{self, ErrorCategory};
9use serde_json::{Value, json};
10use thiserror::Error;
11
12#[derive(Error, Debug)]
14pub enum AttachError {
15 #[error("Terminal error: {0}")]
16 Terminal(#[from] io::Error),
17
18 #[error("PTY write failed: {0}")]
19 PtyWrite(String),
20
21 #[error("PTY read failed: {0}")]
22 PtyRead(String),
23
24 #[error("Event read failed")]
25 EventRead,
26}
27
28impl AttachError {
29 pub fn code(&self) -> i32 {
31 match self {
32 AttachError::Terminal(_) => error_codes::PTY_ERROR,
33 AttachError::PtyWrite(_) => error_codes::PTY_ERROR,
34 AttachError::PtyRead(_) => error_codes::PTY_ERROR,
35 AttachError::EventRead => error_codes::PTY_ERROR,
36 }
37 }
38
39 pub fn category(&self) -> ErrorCategory {
41 ErrorCategory::External
42 }
43
44 pub fn context(&self) -> Value {
46 match self {
47 AttachError::Terminal(e) => json!({
48 "operation": "terminal",
49 "reason": e.to_string()
50 }),
51 AttachError::PtyWrite(reason) => json!({
52 "operation": "pty_write",
53 "reason": reason
54 }),
55 AttachError::PtyRead(reason) => json!({
56 "operation": "pty_read",
57 "reason": reason
58 }),
59 AttachError::EventRead => json!({
60 "operation": "event_read",
61 "reason": "Failed to read terminal events"
62 }),
63 }
64 }
65
66 pub fn suggestion(&self) -> String {
68 match self {
69 AttachError::Terminal(_) => {
70 "Terminal mode error. Try restarting your terminal.".to_string()
71 }
72 AttachError::PtyWrite(_) => {
73 "Failed to send input to session. The session may have ended. Run 'sessions' to check status."
74 .to_string()
75 }
76 AttachError::PtyRead(_) => {
77 "Failed to read from session. The session may have ended. Run 'sessions' to check status."
78 .to_string()
79 }
80 AttachError::EventRead => {
81 "Failed to read terminal events. Try restarting your terminal.".to_string()
82 }
83 }
84 }
85
86 pub fn is_retryable(&self) -> bool {
88 matches!(self, AttachError::PtyWrite(_) | AttachError::PtyRead(_))
89 }
90
91 pub fn exit_code(&self) -> i32 {
93 match self.category() {
94 ErrorCategory::InvalidInput => 64, ErrorCategory::NotFound => 69, ErrorCategory::Busy => 73, ErrorCategory::External => 74, ErrorCategory::Internal => 74, ErrorCategory::Timeout => 75, }
101 }
102
103 pub fn to_json(&self) -> Value {
105 json!({
106 "code": self.code(),
107 "message": self.to_string(),
108 "category": self.category().as_str(),
109 "retryable": self.is_retryable(),
110 "context": self.context(),
111 "suggestion": self.suggestion()
112 })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_attach_error_code() {
122 let err = AttachError::PtyWrite("broken pipe".into());
123 assert_eq!(err.code(), error_codes::PTY_ERROR);
124
125 let err = AttachError::PtyRead("timeout".into());
126 assert_eq!(err.code(), error_codes::PTY_ERROR);
127
128 let err = AttachError::EventRead;
129 assert_eq!(err.code(), error_codes::PTY_ERROR);
130 }
131
132 #[test]
133 fn test_attach_error_category() {
134 let err = AttachError::PtyWrite("x".into());
135 assert_eq!(err.category(), ErrorCategory::External);
136
137 let err = AttachError::EventRead;
138 assert_eq!(err.category(), ErrorCategory::External);
139 }
140
141 #[test]
142 fn test_attach_error_context() {
143 let err = AttachError::PtyWrite("broken pipe".into());
144 let ctx = err.context();
145 assert_eq!(ctx["operation"], "pty_write");
146 assert_eq!(ctx["reason"], "broken pipe");
147
148 let err = AttachError::PtyRead("timeout".into());
149 let ctx = err.context();
150 assert_eq!(ctx["operation"], "pty_read");
151 assert_eq!(ctx["reason"], "timeout");
152 }
153
154 #[test]
155 fn test_attach_error_suggestion() {
156 let err = AttachError::PtyWrite("x".into());
157 assert!(err.suggestion().contains("session"));
158
159 let err = AttachError::EventRead;
160 assert!(err.suggestion().contains("terminal"));
161 }
162
163 #[test]
164 fn test_attach_error_is_retryable() {
165 assert!(AttachError::PtyWrite("x".into()).is_retryable());
166 assert!(AttachError::PtyRead("x".into()).is_retryable());
167 assert!(!AttachError::EventRead.is_retryable());
168 }
169
170 #[test]
171 fn test_attach_error_exit_code() {
172 let err = AttachError::PtyWrite("x".into());
173 assert_eq!(err.exit_code(), 74); }
175
176 #[test]
177 fn test_attach_error_to_json() {
178 let err = AttachError::PtyRead("connection reset".into());
179 let json = err.to_json();
180 assert_eq!(json["code"], error_codes::PTY_ERROR);
181 assert_eq!(json["category"], "external");
182 assert_eq!(json["retryable"], true);
183 assert!(
184 json["context"]["operation"]
185 .as_str()
186 .unwrap()
187 .contains("pty_read")
188 );
189 }
190}