1use super::effect::{AppEffect, AppEffectHandler, AppEffectResult};
31use std::path::PathBuf;
32
33const PLAN_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/plan.xsd");
35const DEVELOPMENT_RESULT_XSD_SCHEMA: &str =
36 include_str!("../files/llm_output_extraction/development_result.xsd");
37const ISSUES_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/issues.xsd");
38const FIX_RESULT_XSD_SCHEMA: &str = include_str!("../files/llm_output_extraction/fix_result.xsd");
39const COMMIT_MESSAGE_XSD_SCHEMA: &str =
40 include_str!("../files/llm_output_extraction/commit_message.xsd");
41
42use crate::files::io::context::{VAGUE_ISSUES_LINE, VAGUE_NOTES_LINE, VAGUE_STATUS_LINE};
44
45pub fn handle_reset_start_commit<H: AppEffectHandler>(
67 handler: &mut H,
68 working_dir_override: Option<&PathBuf>,
69) -> Result<String, String> {
70 if let Some(dir) = working_dir_override {
72 match handler.execute(AppEffect::SetCurrentDir { path: dir.clone() }) {
73 AppEffectResult::Ok => {}
74 AppEffectResult::Error(e) => return Err(e),
75 other => return Err(format!("unexpected result from SetCurrentDir: {other:?}")),
76 }
77 }
78
79 match handler.execute(AppEffect::GitRequireRepo) {
81 AppEffectResult::Ok => {}
82 AppEffectResult::Error(e) => return Err(e),
83 other => return Err(format!("unexpected result from GitRequireRepo: {other:?}")),
84 }
85
86 let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
88 AppEffectResult::Path(p) => p,
89 AppEffectResult::Error(e) => return Err(e),
90 other => return Err(format!("unexpected result from GitGetRepoRoot: {other:?}")),
91 };
92
93 if working_dir_override.is_none() {
95 match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
96 AppEffectResult::Ok => {}
97 AppEffectResult::Error(e) => return Err(e),
98 other => return Err(format!("unexpected result from SetCurrentDir: {other:?}")),
99 }
100 }
101
102 match handler.execute(AppEffect::GitResetStartCommit) {
104 AppEffectResult::String(oid) => Ok(oid),
105 AppEffectResult::Error(e) => Err(e),
106 other => Err(format!(
107 "unexpected result from GitResetStartCommit: {other:?}"
108 )),
109 }
110}
111
112pub fn save_start_commit<H: AppEffectHandler>(handler: &mut H) -> Result<String, String> {
121 match handler.execute(AppEffect::GitSaveStartCommit) {
122 AppEffectResult::String(oid) => Ok(oid),
123 AppEffectResult::Error(e) => Err(e),
124 other => Err(format!(
125 "unexpected result from GitSaveStartCommit: {other:?}"
126 )),
127 }
128}
129
130pub fn is_on_main_branch<H: AppEffectHandler>(handler: &mut H) -> Result<bool, String> {
136 match handler.execute(AppEffect::GitIsMainBranch) {
137 AppEffectResult::Bool(b) => Ok(b),
138 AppEffectResult::Error(e) => Err(e),
139 other => Err(format!("unexpected result from GitIsMainBranch: {other:?}")),
140 }
141}
142
143pub fn get_head_oid<H: AppEffectHandler>(handler: &mut H) -> Result<String, String> {
149 match handler.execute(AppEffect::GitGetHeadOid) {
150 AppEffectResult::String(oid) => Ok(oid),
151 AppEffectResult::Error(e) => Err(e),
152 other => Err(format!("unexpected result from GitGetHeadOid: {other:?}")),
153 }
154}
155
156pub fn require_repo<H: AppEffectHandler>(handler: &mut H) -> Result<(), String> {
162 match handler.execute(AppEffect::GitRequireRepo) {
163 AppEffectResult::Ok => Ok(()),
164 AppEffectResult::Error(e) => Err(e),
165 other => Err(format!("unexpected result from GitRequireRepo: {other:?}")),
166 }
167}
168
169pub fn get_repo_root<H: AppEffectHandler>(handler: &mut H) -> Result<PathBuf, String> {
175 match handler.execute(AppEffect::GitGetRepoRoot) {
176 AppEffectResult::Path(p) => Ok(p),
177 AppEffectResult::Error(e) => Err(e),
178 other => Err(format!("unexpected result from GitGetRepoRoot: {other:?}")),
179 }
180}
181
182pub fn ensure_files_effectful<H: AppEffectHandler>(
206 handler: &mut H,
207 isolation_mode: bool,
208) -> Result<(), String> {
209 match handler.execute(AppEffect::CreateDir {
211 path: PathBuf::from(".agent/logs"),
212 }) {
213 AppEffectResult::Ok => {}
214 AppEffectResult::Error(e) => return Err(format!("Failed to create .agent/logs: {e}")),
215 other => return Err(format!("Unexpected result from CreateDir: {other:?}")),
216 }
217
218 match handler.execute(AppEffect::CreateDir {
220 path: PathBuf::from(".agent/tmp"),
221 }) {
222 AppEffectResult::Ok => {}
223 AppEffectResult::Error(e) => return Err(format!("Failed to create .agent/tmp: {e}")),
224 other => return Err(format!("Unexpected result from CreateDir: {other:?}")),
225 }
226
227 let schemas = [
229 (".agent/tmp/plan.xsd", PLAN_XSD_SCHEMA),
230 (
231 ".agent/tmp/development_result.xsd",
232 DEVELOPMENT_RESULT_XSD_SCHEMA,
233 ),
234 (".agent/tmp/issues.xsd", ISSUES_XSD_SCHEMA),
235 (".agent/tmp/fix_result.xsd", FIX_RESULT_XSD_SCHEMA),
236 (".agent/tmp/commit_message.xsd", COMMIT_MESSAGE_XSD_SCHEMA),
237 ];
238
239 for (path, content) in schemas {
240 match handler.execute(AppEffect::WriteFile {
241 path: PathBuf::from(path),
242 content: content.to_string(),
243 }) {
244 AppEffectResult::Ok => {}
245 AppEffectResult::Error(e) => return Err(format!("Failed to write {path}: {e}")),
246 other => return Err(format!("Unexpected result from WriteFile: {other:?}")),
247 }
248 }
249
250 if !isolation_mode {
252 let context_files = [
253 (".agent/STATUS.md", VAGUE_STATUS_LINE),
254 (".agent/NOTES.md", VAGUE_NOTES_LINE),
255 (".agent/ISSUES.md", VAGUE_ISSUES_LINE),
256 ];
257
258 for (path, line) in context_files {
259 let content = format!("{}\n", line.lines().next().unwrap_or_default().trim());
261 match handler.execute(AppEffect::WriteFile {
262 path: PathBuf::from(path),
263 content,
264 }) {
265 AppEffectResult::Ok => {}
266 AppEffectResult::Error(e) => return Err(format!("Failed to write {path}: {e}")),
267 other => return Err(format!("Unexpected result from WriteFile: {other:?}")),
268 }
269 }
270 }
271
272 Ok(())
273}
274
275pub fn reset_context_for_isolation_effectful<H: AppEffectHandler>(
294 handler: &mut H,
295) -> Result<(), String> {
296 let context_files = [
297 PathBuf::from(".agent/STATUS.md"),
298 PathBuf::from(".agent/NOTES.md"),
299 PathBuf::from(".agent/ISSUES.md"),
300 ];
301
302 for path in context_files {
303 let exists = match handler.execute(AppEffect::PathExists { path: path.clone() }) {
305 AppEffectResult::Bool(b) => b,
306 AppEffectResult::Error(e) => {
307 return Err(format!(
308 "Failed to check if {} exists: {}",
309 path.display(),
310 e
311 ))
312 }
313 other => {
314 return Err(format!(
315 "Unexpected result from PathExists for {}: {:?}",
316 path.display(),
317 other
318 ))
319 }
320 };
321
322 if exists {
324 match handler.execute(AppEffect::DeleteFile { path: path.clone() }) {
325 AppEffectResult::Ok => {}
326 AppEffectResult::Error(e) => {
327 return Err(format!("Failed to delete {}: {}", path.display(), e))
328 }
329 other => {
330 return Err(format!(
331 "Unexpected result from DeleteFile for {}: {:?}",
332 path.display(),
333 other
334 ))
335 }
336 }
337 }
338 }
339
340 Ok(())
341}
342
343pub fn check_prompt_exists_effectful<H: AppEffectHandler>(handler: &mut H) -> Result<bool, String> {
357 match handler.execute(AppEffect::PathExists {
358 path: PathBuf::from("PROMPT.md"),
359 }) {
360 AppEffectResult::Bool(exists) => Ok(exists),
361 AppEffectResult::Error(e) => Err(format!("Failed to check PROMPT.md: {}", e)),
362 other => Err(format!("Unexpected result from PathExists: {:?}", other)),
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::app::mock_effect_handler::MockAppEffectHandler;
370
371 #[test]
372 fn test_reset_start_commit_emits_correct_effects() {
373 let mut handler = MockAppEffectHandler::new();
374
375 let result = handle_reset_start_commit(&mut handler, None);
376
377 assert!(result.is_ok());
378 let captured = handler.captured();
379 assert!(
380 captured
381 .iter()
382 .any(|e| matches!(e, AppEffect::GitRequireRepo)),
383 "should emit GitRequireRepo"
384 );
385 assert!(
386 captured
387 .iter()
388 .any(|e| matches!(e, AppEffect::GitGetRepoRoot)),
389 "should emit GitGetRepoRoot"
390 );
391 assert!(
392 captured
393 .iter()
394 .any(|e| matches!(e, AppEffect::GitResetStartCommit)),
395 "should emit GitResetStartCommit"
396 );
397 }
398
399 #[test]
400 fn test_reset_start_commit_with_working_dir() {
401 let mut handler = MockAppEffectHandler::new();
402 let dir = PathBuf::from("/test/dir");
403
404 let result = handle_reset_start_commit(&mut handler, Some(&dir));
405
406 assert!(result.is_ok());
407 let captured = handler.captured();
408 assert!(
409 captured
410 .iter()
411 .any(|e| matches!(e, AppEffect::SetCurrentDir { path } if path == &dir)),
412 "should emit SetCurrentDir with the override path"
413 );
414 }
415
416 #[test]
417 fn test_reset_start_commit_fails_without_repo() {
418 let mut handler = MockAppEffectHandler::new().without_repo();
419
420 let result = handle_reset_start_commit(&mut handler, None);
421
422 assert!(result.is_err());
423 assert!(result.unwrap_err().contains("git repository"));
424 }
425
426 #[test]
427 fn test_save_start_commit_returns_oid() {
428 let expected_oid = "abc123def456";
429 let mut handler = MockAppEffectHandler::new().with_head_oid(expected_oid);
430
431 let result = save_start_commit(&mut handler);
432
433 assert!(result.is_ok());
434 assert_eq!(result.unwrap(), expected_oid);
435 }
436
437 #[test]
438 fn test_is_on_main_branch_true() {
439 let mut handler = MockAppEffectHandler::new().on_main_branch();
440
441 let result = is_on_main_branch(&mut handler);
442
443 assert!(result.is_ok());
444 assert!(result.unwrap());
445 }
446
447 #[test]
448 fn test_is_on_main_branch_false() {
449 let mut handler = MockAppEffectHandler::new(); let result = is_on_main_branch(&mut handler);
452
453 assert!(result.is_ok());
454 assert!(!result.unwrap());
455 }
456
457 #[test]
458 fn test_get_head_oid() {
459 let expected = "1234567890abcdef1234567890abcdef12345678";
460 let mut handler = MockAppEffectHandler::new().with_head_oid(expected);
461
462 let result = get_head_oid(&mut handler);
463
464 assert!(result.is_ok());
465 assert_eq!(result.unwrap(), expected);
466 }
467
468 #[test]
469 fn test_require_repo_success() {
470 let mut handler = MockAppEffectHandler::new();
471
472 let result = require_repo(&mut handler);
473
474 assert!(result.is_ok());
475 }
476
477 #[test]
478 fn test_require_repo_failure() {
479 let mut handler = MockAppEffectHandler::new().without_repo();
480
481 let result = require_repo(&mut handler);
482
483 assert!(result.is_err());
484 }
485
486 #[test]
487 fn test_get_repo_root() {
488 let mut handler = MockAppEffectHandler::new();
489
490 let result = get_repo_root(&mut handler);
491
492 assert!(result.is_ok());
493 assert_eq!(result.unwrap(), PathBuf::from("/"));
495 }
496
497 #[test]
498 fn test_ensure_files_creates_directories() {
499 let mut handler = MockAppEffectHandler::new();
500
501 let result = ensure_files_effectful(&mut handler, true);
502
503 assert!(result.is_ok());
504
505 let captured = handler.captured();
507 assert!(
508 captured.iter().any(
509 |e| matches!(e, AppEffect::CreateDir { path } if path.ends_with(".agent/logs"))
510 ),
511 "should create .agent/logs directory"
512 );
513 assert!(
514 captured.iter().any(
515 |e| matches!(e, AppEffect::CreateDir { path } if path.ends_with(".agent/tmp"))
516 ),
517 "should create .agent/tmp directory"
518 );
519 }
520
521 #[test]
522 fn test_ensure_files_writes_xsd_schemas() {
523 let mut handler = MockAppEffectHandler::new();
524
525 let result = ensure_files_effectful(&mut handler, true);
526
527 assert!(result.is_ok());
528
529 let captured = handler.captured();
531 assert!(
532 captured.iter().any(
533 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("plan.xsd"))
534 ),
535 "should write plan.xsd"
536 );
537 assert!(
538 captured.iter().any(
539 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("issues.xsd"))
540 ),
541 "should write issues.xsd"
542 );
543 }
544
545 #[test]
546 fn test_ensure_files_non_isolation_creates_context_files() {
547 let mut handler = MockAppEffectHandler::new();
548
549 let result = ensure_files_effectful(&mut handler, false);
551
552 assert!(result.is_ok());
553
554 let captured = handler.captured();
555 assert!(
556 captured.iter().any(
557 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("STATUS.md"))
558 ),
559 "should create STATUS.md in non-isolation mode"
560 );
561 assert!(
562 captured.iter().any(
563 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("NOTES.md"))
564 ),
565 "should create NOTES.md in non-isolation mode"
566 );
567 assert!(
568 captured.iter().any(
569 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("ISSUES.md"))
570 ),
571 "should create ISSUES.md in non-isolation mode"
572 );
573 }
574
575 #[test]
576 fn test_ensure_files_isolation_skips_context_files() {
577 let mut handler = MockAppEffectHandler::new();
578
579 let result = ensure_files_effectful(&mut handler, true);
581
582 assert!(result.is_ok());
583
584 let captured = handler.captured();
585 assert!(
586 !captured.iter().any(
587 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("STATUS.md"))
588 ),
589 "should NOT create STATUS.md in isolation mode"
590 );
591 assert!(
592 !captured.iter().any(
593 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("NOTES.md"))
594 ),
595 "should NOT create NOTES.md in isolation mode"
596 );
597 assert!(
598 !captured.iter().any(
599 |e| matches!(e, AppEffect::WriteFile { path, .. } if path.ends_with("ISSUES.md"))
600 ),
601 "should NOT create ISSUES.md in isolation mode"
602 );
603 }
604
605 #[test]
606 fn test_reset_context_for_isolation_deletes_existing_files() {
607 let mut handler = MockAppEffectHandler::new()
609 .with_file(".agent/STATUS.md", "old status")
610 .with_file(".agent/NOTES.md", "old notes")
611 .with_file(".agent/ISSUES.md", "old issues");
612
613 let result = reset_context_for_isolation_effectful(&mut handler);
614
615 assert!(result.is_ok());
616
617 let captured = handler.captured();
618 assert!(
619 captured.iter().any(
620 |e| matches!(e, AppEffect::DeleteFile { path } if path.ends_with("STATUS.md"))
621 ),
622 "should delete STATUS.md"
623 );
624 assert!(
625 captured
626 .iter()
627 .any(|e| matches!(e, AppEffect::DeleteFile { path } if path.ends_with("NOTES.md"))),
628 "should delete NOTES.md"
629 );
630 assert!(
631 captured.iter().any(
632 |e| matches!(e, AppEffect::DeleteFile { path } if path.ends_with("ISSUES.md"))
633 ),
634 "should delete ISSUES.md"
635 );
636 }
637
638 #[test]
639 fn test_reset_context_for_isolation_skips_nonexistent_files() {
640 let mut handler = MockAppEffectHandler::new();
642
643 let result = reset_context_for_isolation_effectful(&mut handler);
644
645 assert!(result.is_ok());
646
647 let captured = handler.captured();
648 assert!(
650 captured.iter().any(
651 |e| matches!(e, AppEffect::PathExists { path } if path.ends_with("STATUS.md"))
652 ),
653 "should check if STATUS.md exists"
654 );
655 assert!(
657 !captured.iter().any(
658 |e| matches!(e, AppEffect::DeleteFile { path } if path.ends_with("STATUS.md"))
659 ),
660 "should NOT delete non-existent STATUS.md"
661 );
662 }
663
664 #[test]
665 fn test_check_prompt_exists_returns_true_when_file_exists() {
666 let mut handler = MockAppEffectHandler::new().with_file("PROMPT.md", "# Goal\nTest");
667
668 let result = check_prompt_exists_effectful(&mut handler);
669
670 assert!(result.is_ok());
671 assert!(result.unwrap(), "should return true when PROMPT.md exists");
672 }
673
674 #[test]
675 fn test_check_prompt_exists_returns_false_when_file_missing() {
676 let mut handler = MockAppEffectHandler::new();
677
678 let result = check_prompt_exists_effectful(&mut handler);
679
680 assert!(result.is_ok());
681 assert!(
682 !result.unwrap(),
683 "should return false when PROMPT.md doesn't exist"
684 );
685 }
686}