1use thiserror::Error;
10
11#[derive(Debug, Error)]
12pub enum CoderError {
13 #[error("workspace error: {0}")]
14 Workspace(String),
15 #[error("prompt too large: {actual} chars > cap {cap}")]
16 PromptTooLarge { actual: usize, cap: usize },
17 #[error("emission malformed: {0}")]
18 BadEmission(String),
19 #[error("file write failed: {0}")]
20 FileWrite(String),
21 #[error("file write failed: empty emission for '{path}'")]
28 EmptyEmission { path: String },
29 #[error("file write failed: emission for '{path}' looks like a diff, not a whole file")]
33 LooksLikeDiff { path: String },
34 #[error("file write failed: emission for '{path}' still begins with a leaked FILE: marker")]
38 LeakedMarker { path: String },
39 #[error("inference: {0}")]
40 Inference(String),
41 #[error("capability denied: {kind} does not permit '{target}'")]
51 CapabilityDenied { kind: &'static str, target: String },
52 #[error("io: {0}")]
53 Io(#[from] std::io::Error),
54}
55
56pub type Result<T> = std::result::Result<T, CoderError>;
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn workspace_error_renders() {
64 let e = CoderError::Workspace("missing dir".to_string());
65 assert!(e.to_string().contains("missing dir"));
66 }
67
68 #[test]
69 fn prompt_too_large_renders() {
70 let e = CoderError::PromptTooLarge {
71 actual: 100,
72 cap: 50,
73 };
74 let s = e.to_string();
75 assert!(s.contains("100"));
76 assert!(s.contains("50"));
77 }
78
79 #[test]
80 fn bad_emission_renders() {
81 let e = CoderError::BadEmission("no FILE: header".to_string());
82 assert!(e.to_string().contains("no FILE: header"));
83 }
84
85 #[test]
86 fn file_write_renders() {
87 let e = CoderError::FileWrite("permission denied".to_string());
88 assert!(e.to_string().contains("permission denied"));
89 }
90
91 #[test]
92 fn empty_emission_renders_with_prefix() {
93 let e = CoderError::EmptyEmission {
94 path: "src/lib.rs".to_string(),
95 };
96 let s = e.to_string();
97 assert!(s.starts_with("file write failed:"), "got: {s}");
98 assert!(s.contains("src/lib.rs"));
99 }
100
101 #[test]
102 fn looks_like_diff_renders_with_prefix() {
103 let e = CoderError::LooksLikeDiff {
104 path: "src/lib.rs".to_string(),
105 };
106 let s = e.to_string();
107 assert!(s.starts_with("file write failed:"), "got: {s}");
108 assert!(s.contains("diff"));
109 }
110
111 #[test]
112 fn leaked_marker_renders_with_prefix() {
113 let e = CoderError::LeakedMarker {
114 path: "src/lib.rs".to_string(),
115 };
116 let s = e.to_string();
117 assert!(s.starts_with("file write failed:"), "got: {s}");
118 assert!(s.contains("FILE:"));
119 }
120
121 #[test]
122 fn inference_renders() {
123 let e = CoderError::Inference("backend offline".to_string());
124 assert!(e.to_string().contains("backend offline"));
125 }
126
127 #[test]
128 fn io_error_converts() {
129 let io: std::io::Error = std::io::Error::new(std::io::ErrorKind::NotFound, "x");
130 let e: CoderError = io.into();
131 assert!(matches!(e, CoderError::Io(_)));
132 }
133
134 #[test]
135 fn capability_denied_renders_kind_and_target() {
136 let e = CoderError::CapabilityDenied {
137 kind: "fs_write",
138 target: "forbidden.rs".to_string(),
139 };
140 let s = e.to_string();
141 assert!(s.contains("capability denied"));
142 assert!(s.contains("fs_write"));
143 assert!(s.contains("forbidden.rs"));
144 }
145
146 #[test]
147 fn capability_denied_kinds_match_dispatch_axes() {
148 for kind in ["fs_read", "fs_write", "net", "exec", "max_calls"] {
149 let e = CoderError::CapabilityDenied {
150 kind,
151 target: "x".to_string(),
152 };
153 assert!(e.to_string().contains(kind));
154 }
155 }
156}