Skip to main content

codex_feedback/
feedback_diagnostics.rs

1use std::collections::HashMap;
2
3pub const FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME: &str = "codex-connectivity-diagnostics.txt";
4const PROXY_ENV_VARS: &[&str] = &[
5    "HTTP_PROXY",
6    "http_proxy",
7    "HTTPS_PROXY",
8    "https_proxy",
9    "ALL_PROXY",
10    "all_proxy",
11];
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct FeedbackDiagnostics {
15    diagnostics: Vec<FeedbackDiagnostic>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct FeedbackDiagnostic {
20    pub headline: String,
21    pub details: Vec<String>,
22}
23
24impl FeedbackDiagnostics {
25    pub fn new(diagnostics: Vec<FeedbackDiagnostic>) -> Self {
26        Self { diagnostics }
27    }
28
29    pub fn collect_from_env() -> Self {
30        Self::collect_from_pairs(std::env::vars())
31    }
32
33    fn collect_from_pairs<I, K, V>(pairs: I) -> Self
34    where
35        I: IntoIterator<Item = (K, V)>,
36        K: Into<String>,
37        V: Into<String>,
38    {
39        let env = pairs
40            .into_iter()
41            .map(|(key, value)| (key.into(), value.into()))
42            .collect::<HashMap<_, _>>();
43        let mut diagnostics = Vec::new();
44
45        let proxy_details = PROXY_ENV_VARS
46            .iter()
47            .filter_map(|key| {
48                let value = env.get(*key)?;
49                Some(format!("{key} = {value}"))
50            })
51            .collect::<Vec<_>>();
52        if !proxy_details.is_empty() {
53            diagnostics.push(FeedbackDiagnostic {
54                headline: "Proxy environment variables are set and may affect connectivity."
55                    .to_string(),
56                details: proxy_details,
57            });
58        }
59
60        Self { diagnostics }
61    }
62
63    pub fn is_empty(&self) -> bool {
64        self.diagnostics.is_empty()
65    }
66
67    pub fn diagnostics(&self) -> &[FeedbackDiagnostic] {
68        &self.diagnostics
69    }
70
71    pub fn attachment_text(&self) -> Option<String> {
72        if self.diagnostics.is_empty() {
73            return None;
74        }
75
76        let mut lines = vec!["Connectivity diagnostics".to_string(), String::new()];
77        for diagnostic in &self.diagnostics {
78            lines.push(format!("- {}", diagnostic.headline));
79            lines.extend(
80                diagnostic
81                    .details
82                    .iter()
83                    .map(|detail| format!("  - {detail}")),
84            );
85        }
86
87        Some(lines.join("\n"))
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use pretty_assertions::assert_eq;
94
95    use super::FeedbackDiagnostic;
96    use super::FeedbackDiagnostics;
97
98    #[test]
99    fn collect_from_pairs_reports_raw_values_and_attachment() {
100        let diagnostics = FeedbackDiagnostics::collect_from_pairs([
101            (
102                "HTTPS_PROXY",
103                "https://user:password@secure-proxy.example.com:443?secret=1",
104            ),
105            ("http_proxy", "proxy.example.com:8080"),
106            ("all_proxy", "socks5h://all-proxy.example.com:1080"),
107        ]);
108
109        assert_eq!(
110            diagnostics,
111            FeedbackDiagnostics {
112                diagnostics: vec![FeedbackDiagnostic {
113                    headline: "Proxy environment variables are set and may affect connectivity."
114                        .to_string(),
115                    details: vec![
116                        "http_proxy = proxy.example.com:8080".to_string(),
117                        "HTTPS_PROXY = https://user:password@secure-proxy.example.com:443?secret=1"
118                            .to_string(),
119                        "all_proxy = socks5h://all-proxy.example.com:1080".to_string(),
120                    ],
121                },],
122            }
123        );
124
125        assert_eq!(
126            diagnostics.attachment_text(),
127            Some(
128                r#"Connectivity diagnostics
129
130- Proxy environment variables are set and may affect connectivity.
131  - http_proxy = proxy.example.com:8080
132  - HTTPS_PROXY = https://user:password@secure-proxy.example.com:443?secret=1
133  - all_proxy = socks5h://all-proxy.example.com:1080"#
134                    .to_string()
135            )
136        );
137    }
138
139    #[test]
140    fn collect_from_pairs_ignores_absent_values() {
141        let diagnostics = FeedbackDiagnostics::collect_from_pairs(Vec::<(String, String)>::new());
142        assert_eq!(diagnostics, FeedbackDiagnostics::default());
143        assert_eq!(diagnostics.attachment_text(), None);
144    }
145
146    #[test]
147    fn collect_from_pairs_preserves_whitespace_and_empty_values() {
148        let diagnostics =
149            FeedbackDiagnostics::collect_from_pairs([("HTTP_PROXY", "  proxy with spaces  ")]);
150
151        assert_eq!(
152            diagnostics,
153            FeedbackDiagnostics {
154                diagnostics: vec![FeedbackDiagnostic {
155                    headline: "Proxy environment variables are set and may affect connectivity."
156                        .to_string(),
157                    details: vec!["HTTP_PROXY =   proxy with spaces  ".to_string()],
158                },],
159            }
160        );
161    }
162
163    #[test]
164    fn collect_from_pairs_reports_values_verbatim() {
165        let proxy_value = "not a valid proxy";
166        let diagnostics = FeedbackDiagnostics::collect_from_pairs([("HTTP_PROXY", proxy_value)]);
167
168        assert_eq!(
169            diagnostics,
170            FeedbackDiagnostics {
171                diagnostics: vec![FeedbackDiagnostic {
172                    headline: "Proxy environment variables are set and may affect connectivity."
173                        .to_string(),
174                    details: vec!["HTTP_PROXY = not a valid proxy".to_string()],
175                },],
176            }
177        );
178    }
179}