agentstatedeveloper_core/
sidecar_config.rs1use serde::Deserialize;
24use std::path::Path;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum ShardBy {
29 #[default]
32 Class,
33 Package,
38}
39
40#[derive(Debug, Clone, Copy)]
44pub struct BudgetConfig {
45 pub total_bytes: u64,
48 pub per_shard_bytes: u64,
51}
52
53impl Default for BudgetConfig {
54 fn default() -> Self {
55 Self {
56 total_bytes: 1024 * 1024,
57 per_shard_bytes: 200 * 1024,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct SidecarConfig {
65 pub shard_by: ShardBy,
66 pub budget: BudgetConfig,
67}
68
69#[derive(Debug, Deserialize, Default)]
70struct ConfigFile {
71 #[serde(default)]
72 sidecar: Option<SidecarSection>,
73}
74
75#[derive(Debug, Deserialize, Default)]
76struct SidecarSection {
77 #[serde(default)]
80 shard_by: Option<String>,
81 #[serde(default)]
84 budget_total_bytes: Option<u64>,
85 #[serde(default)]
88 budget_per_shard_bytes: Option<u64>,
89}
90
91impl SidecarConfig {
92 pub fn load_from_project(project_root: &Path) -> Self {
96 let path = project_root.join(".asd").join("config.toml");
97 Self::load_from_file(&path)
98 }
99
100 pub fn load_from_file(path: &Path) -> Self {
102 let raw = match std::fs::read_to_string(path) {
103 Ok(s) => s,
104 Err(_) => return Self::default(),
105 };
106 let parsed: ConfigFile = match toml::from_str(&raw) {
107 Ok(p) => p,
108 Err(_) => return Self::default(),
109 };
110 let sidecar = parsed.sidecar.unwrap_or_default();
111 let shard_by = sidecar
112 .shard_by
113 .as_deref()
114 .map(|s| match s {
115 "package" => ShardBy::Package,
116 "class" => ShardBy::Class,
117 _ => ShardBy::Class, })
119 .unwrap_or_default();
120 let default_budget = BudgetConfig::default();
121 let budget = BudgetConfig {
122 total_bytes: sidecar
123 .budget_total_bytes
124 .unwrap_or(default_budget.total_bytes),
125 per_shard_bytes: sidecar
126 .budget_per_shard_bytes
127 .unwrap_or(default_budget.per_shard_bytes),
128 };
129 Self { shard_by, budget }
130 }
131}
132
133pub fn package_key_for_filename(package_dir: &str) -> String {
140 if package_dir.is_empty() {
141 return "_root".to_string();
142 }
143 package_dir.replace('/', "--").replace('\\', "--")
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use tempfile::tempdir;
150
151 #[test]
152 fn defaults_when_no_config_file() {
153 let tmp = tempdir().unwrap();
154 let cfg = SidecarConfig::load_from_project(tmp.path());
155 assert_eq!(cfg.shard_by, ShardBy::Class);
156 }
157
158 #[test]
159 fn reads_shard_by_package() {
160 let tmp = tempdir().unwrap();
161 let asd = tmp.path().join(".asd");
162 std::fs::create_dir_all(&asd).unwrap();
163 std::fs::write(
164 asd.join("config.toml"),
165 "[sidecar]\nshard_by = \"package\"\n",
166 )
167 .unwrap();
168 let cfg = SidecarConfig::load_from_project(tmp.path());
169 assert_eq!(cfg.shard_by, ShardBy::Package);
170 }
171
172 #[test]
173 fn reads_shard_by_class_explicitly() {
174 let tmp = tempdir().unwrap();
175 let asd = tmp.path().join(".asd");
176 std::fs::create_dir_all(&asd).unwrap();
177 std::fs::write(asd.join("config.toml"), "[sidecar]\nshard_by = \"class\"\n").unwrap();
178 let cfg = SidecarConfig::load_from_project(tmp.path());
179 assert_eq!(cfg.shard_by, ShardBy::Class);
180 }
181
182 #[test]
183 fn unknown_value_falls_back_to_default() {
184 let tmp = tempdir().unwrap();
185 let asd = tmp.path().join(".asd");
186 std::fs::create_dir_all(&asd).unwrap();
187 std::fs::write(
188 asd.join("config.toml"),
189 "[sidecar]\nshard_by = \"banana\"\n",
190 )
191 .unwrap();
192 let cfg = SidecarConfig::load_from_project(tmp.path());
193 assert_eq!(
194 cfg.shard_by,
195 ShardBy::Class,
196 "unknown shard_by value must fall back to default"
197 );
198 }
199
200 #[test]
201 fn malformed_toml_falls_back_to_default() {
202 let tmp = tempdir().unwrap();
203 let asd = tmp.path().join(".asd");
204 std::fs::create_dir_all(&asd).unwrap();
205 std::fs::write(asd.join("config.toml"), "this is not toml {{{").unwrap();
206 let cfg = SidecarConfig::load_from_project(tmp.path());
207 assert_eq!(cfg.shard_by, ShardBy::Class);
208 }
209
210 #[test]
211 fn reads_budget_overrides() {
212 let tmp = tempdir().unwrap();
213 let asd = tmp.path().join(".asd");
214 std::fs::create_dir_all(&asd).unwrap();
215 std::fs::write(
216 asd.join("config.toml"),
217 "[sidecar]\nbudget_total_bytes = 5000\nbudget_per_shard_bytes = 1000\n",
218 )
219 .unwrap();
220 let cfg = SidecarConfig::load_from_project(tmp.path());
221 assert_eq!(cfg.budget.total_bytes, 5000);
222 assert_eq!(cfg.budget.per_shard_bytes, 1000);
223 }
224
225 #[test]
226 fn budget_defaults_when_not_specified() {
227 let tmp = tempdir().unwrap();
228 let cfg = SidecarConfig::load_from_project(tmp.path());
229 assert_eq!(cfg.budget.total_bytes, 1024 * 1024);
230 assert_eq!(cfg.budget.per_shard_bytes, 200 * 1024);
231 }
232
233 #[test]
234 fn package_key_sanitizes_slashes() {
235 assert_eq!(
236 package_key_for_filename("crates/foo/src"),
237 "crates--foo--src"
238 );
239 assert_eq!(package_key_for_filename("src"), "src");
240 assert_eq!(package_key_for_filename(""), "_root");
241 }
242}