talon_core/inference/
error.rs1use std::sync::OnceLock;
8
9use regex::Regex;
10use thiserror::Error;
11
12pub const MAX_DIAGNOSTIC_CHARS: usize = 280;
14
15#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum InferenceError {
19 #[error("inference client build failed: {message}")]
21 Build {
22 message: String,
24 },
25
26 #[error("inference HTTP error{}: {message}", .status.map(|s| format!(" ({s})")).unwrap_or_default())]
28 Http {
29 status: Option<u16>,
31 message: String,
33 },
34
35 #[error("inference response decode failed: {message}")]
37 Decode {
38 message: String,
40 },
41
42 #[error("inference config error: {message}")]
44 Config {
45 message: String,
47 },
48}
49
50fn compile_static(pattern: &str) -> Regex {
54 match Regex::new(pattern) {
55 Ok(re) => re,
56 Err(err) => unreachable!("static regex {pattern:?} did not compile: {err}"),
57 }
58}
59
60fn url_re() -> &'static Regex {
61 static R: OnceLock<Regex> = OnceLock::new();
62 R.get_or_init(|| compile_static(r"https?://[^\s]+"))
63}
64
65fn users_path_re() -> &'static Regex {
66 static R: OnceLock<Regex> = OnceLock::new();
67 R.get_or_init(|| compile_static(r"/Users/[^\s]+"))
68}
69
70fn home_path_re() -> &'static Regex {
71 static R: OnceLock<Regex> = OnceLock::new();
72 R.get_or_init(|| compile_static(r"/home/[^\s]+"))
73}
74
75#[must_use]
82pub fn redact(value: &str) -> String {
83 let stage1 = url_re().replace_all(value, "[sidecar]");
84 let stage2 = users_path_re().replace_all(stage1.as_ref(), "[host-path]");
85 let stage3 = home_path_re().replace_all(stage2.as_ref(), "[host-path]");
86 stage3.chars().take(MAX_DIAGNOSTIC_CHARS).collect()
87}
88
89#[cfg(test)]
90#[allow(clippy::unwrap_used, clippy::expect_used)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn redact_replaces_https_url() {
96 let input = "GET https://localhost:8080/embed failed";
97 let out = redact(input);
98 assert!(out.contains("[sidecar]"));
99 assert!(!out.contains("https://"));
100 }
101
102 #[test]
103 fn redact_replaces_http_url() {
104 let input = "POST http://example.com/api 500";
105 let out = redact(input);
106 assert!(out.contains("[sidecar]"));
107 assert!(!out.contains("example.com"));
108 }
109
110 #[test]
111 fn redact_replaces_users_path() {
112 let input = "/Users/alice/Documents/vault/note.md not found";
113 let out = redact(input);
114 assert!(out.contains("[host-path]"));
115 assert!(!out.contains("alice"));
116 }
117
118 #[test]
119 fn redact_replaces_home_path() {
120 let input = "open /home/bob/talon/idx.sqlite";
121 let out = redact(input);
122 assert!(out.contains("[host-path]"));
123 assert!(!out.contains("bob"));
124 }
125
126 #[test]
127 fn redact_truncates_to_max_chars() {
128 let long = "x".repeat(MAX_DIAGNOSTIC_CHARS + 100);
129 let out = redact(&long);
130 assert_eq!(out.chars().count(), MAX_DIAGNOSTIC_CHARS);
131 }
132
133 #[test]
134 fn redact_passes_through_short_clean_input() {
135 let input = "embedding dimension mismatch";
136 assert_eq!(redact(input), input);
137 }
138
139 #[test]
140 fn redact_handles_multiple_urls_in_one_message() {
141 let input = "fetch https://a.com/x and http://b.com/y both failed";
142 let out = redact(input);
143 assert!(!out.contains("a.com"));
144 assert!(!out.contains("b.com"));
145 }
146}