saorsa_agent/context/
system.rs1use crate::error::Result;
4use std::path::{Path, PathBuf};
5
6use super::types::SystemMode;
7
8#[derive(Debug, Clone, Default)]
10pub struct SystemContext {
11 pub content: String,
13}
14
15impl SystemContext {
16 pub fn load_and_merge(paths: &[PathBuf]) -> Result<Self> {
21 if paths.is_empty() {
22 return Ok(Self {
23 content: String::new(),
24 });
25 }
26
27 let mode = parse_system_mode(&paths[0]).unwrap_or_default();
29
30 let content = match mode {
31 SystemMode::Replace => {
32 load_file(&paths[0])?
34 }
35 SystemMode::Append => {
36 let mut merged = String::new();
38 for (i, path) in paths.iter().enumerate() {
39 let file_content = load_file(path)?;
40 merged.push_str(&file_content);
41 if i < paths.len() - 1 {
43 merged.push_str("\n\n---\n\n");
44 }
45 }
46 merged
47 }
48 };
49
50 Ok(Self { content })
51 }
52
53 pub fn apply_to_default(&self, default: &str, mode: SystemMode) -> String {
58 match mode {
59 SystemMode::Replace => {
60 if self.content.is_empty() {
61 default.to_string()
62 } else {
63 self.content.clone()
64 }
65 }
66 SystemMode::Append => {
67 if self.content.is_empty() {
68 default.to_string()
69 } else {
70 format!("{}\n\n{}", default, self.content)
71 }
72 }
73 }
74 }
75}
76
77fn load_file(path: &Path) -> Result<String> {
79 let content = std::fs::read_to_string(path)?;
80 Ok(strip_front_matter(&content))
81}
82
83fn parse_system_mode(path: &Path) -> Option<SystemMode> {
92 let content = std::fs::read_to_string(path).ok()?;
93 let front_matter = extract_front_matter(&content)?;
94
95 for line in front_matter.lines() {
96 let line = line.trim();
97 if let Some(value) = line.strip_prefix("mode:") {
98 let value = value.trim();
99 return match value {
100 "replace" => Some(SystemMode::Replace),
101 "append" => Some(SystemMode::Append),
102 _ => None,
103 };
104 }
105 }
106 None
107}
108
109fn extract_front_matter(content: &str) -> Option<String> {
111 let trimmed = content.trim_start();
112 if !trimmed.starts_with("---") {
113 return None;
114 }
115
116 let after_first = &trimmed[3..];
117 after_first
118 .find("---")
119 .map(|end_pos| after_first[..end_pos].to_string())
120}
121
122fn strip_front_matter(content: &str) -> String {
124 let trimmed = content.trim_start();
125 if !trimmed.starts_with("---") {
126 return content.to_string();
127 }
128
129 let after_first = &trimmed[3..];
130 if let Some(end_pos) = after_first.find("---") {
131 let body_start = end_pos + 3; after_first[body_start..].trim_start().to_string()
133 } else {
134 content.to_string()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::fs;
142 use tempfile::TempDir;
143
144 fn make_temp_dir() -> TempDir {
145 match TempDir::new() {
146 Ok(t) => t,
147 Err(e) => unreachable!("Failed to create temp dir: {e}"),
148 }
149 }
150
151 fn create_file(dir: &Path, name: &str, content: &str) -> PathBuf {
152 let path = dir.join(name);
153 assert!(fs::write(&path, content).is_ok());
154 path
155 }
156
157 #[test]
158 fn test_load_single_file() {
159 let temp = make_temp_dir();
160 let path = create_file(temp.path(), "SYSTEM.md", "Custom system prompt");
161
162 let result = SystemContext::load_and_merge(&[path]);
163 assert!(result.is_ok());
164
165 let ctx = match result {
166 Ok(c) => c,
167 Err(_) => unreachable!("load_and_merge should succeed"),
168 };
169 assert_eq!(ctx.content, "Custom system prompt");
170 }
171
172 #[test]
173 fn test_append_mode_merges_files() {
174 let temp = make_temp_dir();
175 let path1 = create_file(temp.path(), "SYSTEM1.md", "First instruction");
176 let path2 = create_file(temp.path(), "SYSTEM2.md", "Second instruction");
177
178 let result = SystemContext::load_and_merge(&[path1, path2]);
179 assert!(result.is_ok());
180
181 let ctx = match result {
182 Ok(c) => c,
183 Err(_) => unreachable!("load_and_merge should succeed"),
184 };
185 assert_eq!(
186 ctx.content,
187 "First instruction\n\n---\n\nSecond instruction"
188 );
189 }
190
191 #[test]
192 fn test_replace_mode_uses_first_only() {
193 let temp = make_temp_dir();
194 let content1 = "---\nmode: replace\n---\nFirst instruction";
195 let content2 = "Second instruction";
196
197 let path1 = create_file(temp.path(), "SYSTEM1.md", content1);
198 let path2 = create_file(temp.path(), "SYSTEM2.md", content2);
199
200 let result = SystemContext::load_and_merge(&[path1, path2]);
201 assert!(result.is_ok());
202
203 let ctx = match result {
204 Ok(c) => c,
205 Err(_) => unreachable!("load_and_merge should succeed"),
206 };
207 assert_eq!(ctx.content, "First instruction");
208 }
209
210 #[test]
211 fn test_apply_to_default_append() {
212 let ctx = SystemContext {
213 content: "Custom addition".to_string(),
214 };
215 let result = ctx.apply_to_default("Default prompt", SystemMode::Append);
216 assert_eq!(result, "Default prompt\n\nCustom addition");
217 }
218
219 #[test]
220 fn test_apply_to_default_replace() {
221 let ctx = SystemContext {
222 content: "Completely custom".to_string(),
223 };
224 let result = ctx.apply_to_default("Default prompt", SystemMode::Replace);
225 assert_eq!(result, "Completely custom");
226 }
227
228 #[test]
229 fn test_apply_to_default_empty_content_append() {
230 let ctx = SystemContext {
231 content: String::new(),
232 };
233 let result = ctx.apply_to_default("Default prompt", SystemMode::Append);
234 assert_eq!(result, "Default prompt");
235 }
236
237 #[test]
238 fn test_apply_to_default_empty_content_replace() {
239 let ctx = SystemContext {
240 content: String::new(),
241 };
242 let result = ctx.apply_to_default("Default prompt", SystemMode::Replace);
243 assert_eq!(result, "Default prompt");
244 }
245
246 #[test]
247 fn test_front_matter_parsing() {
248 let temp = make_temp_dir();
249 let content = "---\nmode: replace\n---\nBody content";
250 let path = create_file(temp.path(), "SYSTEM.md", content);
251
252 let mode = parse_system_mode(&path);
253 assert_eq!(mode, Some(SystemMode::Replace));
254 }
255
256 #[test]
257 fn test_front_matter_stripping() {
258 let content = "---\nmode: append\n---\nBody content";
259 let stripped = strip_front_matter(content);
260 assert_eq!(stripped, "Body content");
261 }
262
263 #[test]
264 fn test_no_front_matter() {
265 let content = "Body content only";
266 let stripped = strip_front_matter(content);
267 assert_eq!(stripped, "Body content only");
268 }
269
270 #[test]
271 fn test_empty_file_list() {
272 let result = SystemContext::load_and_merge(&[]);
273 assert!(result.is_ok());
274
275 let ctx = match result {
276 Ok(c) => c,
277 Err(_) => unreachable!("load_and_merge should succeed"),
278 };
279 assert!(ctx.content.is_empty());
280 }
281
282 #[test]
283 fn test_file_read_error_propagated() {
284 let nonexistent = PathBuf::from("/nonexistent/SYSTEM.md");
285 let result = SystemContext::load_and_merge(&[nonexistent]);
286 assert!(result.is_err());
287 }
288
289 #[test]
290 fn test_extract_front_matter_valid() {
291 let content = "---\nmode: append\nother: value\n---\nBody";
292 let fm = extract_front_matter(content);
293 assert!(fm.is_some());
294 match fm {
295 Some(f) => assert!(f.contains("mode: append")),
296 None => unreachable!("Should extract front matter"),
297 }
298 }
299
300 #[test]
301 fn test_extract_front_matter_none_when_missing() {
302 let content = "No front matter here";
303 let fm = extract_front_matter(content);
304 assert!(fm.is_none());
305 }
306
307 #[test]
308 fn test_system_mode_default() {
309 assert_eq!(SystemMode::default(), SystemMode::Append);
310 }
311}