An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1use chrono::Utc;
2use rustagent::config::Config;
3use rustagent::ralph::RalphLoop;
4use rustagent::spec::{Spec, Task, TaskStatus};
5use std::fs;
6use tempfile::TempDir;
7
8#[tokio::test]
9async fn test_ralph_loop_creation() {
10 let temp = TempDir::new().unwrap();
11 let config_path = temp.path().join("config.toml");
12 let spec_path = temp.path().join("test.json");
13
14 fs::write(
15 &config_path,
16 r#"
17[llm]
18provider = "anthropic"
19model = "claude-sonnet-4-20250514"
20
21[anthropic]
22api_key = "test-key"
23
24[rustagent]
25spec_dir = "specs"
26"#,
27 )
28 .unwrap();
29
30 let spec = Spec {
31 name: "test".to_string(),
32 description: "Test".to_string(),
33 branch_name: "feature/test".to_string(),
34 created_at: Utc::now(),
35 tasks: vec![],
36 learnings: vec![],
37 };
38 spec.save(&spec_path).unwrap();
39
40 let config = Config::load(&config_path).unwrap();
41 let ralph = RalphLoop::new(config, spec_path.to_str().unwrap().to_string(), None).unwrap();
42
43 assert!(ralph.spec_path.ends_with("test.json"));
44}
45
46#[test]
47fn test_find_next_pending_task() {
48 let spec = Spec {
49 name: "test".to_string(),
50 description: "Test".to_string(),
51 branch_name: "feature/test".to_string(),
52 created_at: Utc::now(),
53 tasks: vec![
54 Task {
55 id: "task-1".to_string(),
56 title: "Task 1".to_string(),
57 description: "First task".to_string(),
58 acceptance_criteria: vec![],
59 status: TaskStatus::Complete,
60 blocked_reason: None,
61 completed_at: Some(Utc::now()),
62 },
63 Task {
64 id: "task-2".to_string(),
65 title: "Task 2".to_string(),
66 description: "Second task".to_string(),
67 acceptance_criteria: vec![],
68 status: TaskStatus::Pending,
69 blocked_reason: None,
70 completed_at: None,
71 },
72 ],
73 learnings: vec![],
74 };
75
76 let next = spec.find_next_task();
77 assert!(next.is_some());
78 assert_eq!(next.unwrap().id, "task-2");
79}
80
81#[test]
82fn test_find_next_task_skips_blocked() {
83 let spec = Spec {
84 name: "test".to_string(),
85 description: "Test".to_string(),
86 branch_name: "feature/test".to_string(),
87 created_at: Utc::now(),
88 tasks: vec![
89 Task {
90 id: "task-1".to_string(),
91 title: "Task 1".to_string(),
92 description: "First task".to_string(),
93 acceptance_criteria: vec![],
94 status: TaskStatus::Blocked,
95 blocked_reason: Some("Missing dep".to_string()),
96 completed_at: None,
97 },
98 Task {
99 id: "task-2".to_string(),
100 title: "Task 2".to_string(),
101 description: "Second task".to_string(),
102 acceptance_criteria: vec![],
103 status: TaskStatus::Pending,
104 blocked_reason: None,
105 completed_at: None,
106 },
107 ],
108 learnings: vec![],
109 };
110
111 let next = spec.find_next_task();
112 assert!(next.is_some());
113 assert_eq!(next.unwrap().id, "task-2");
114}
115
116#[test]
117fn test_find_next_task_skips_in_progress() {
118 let spec = Spec {
119 name: "test".to_string(),
120 description: "Test".to_string(),
121 branch_name: "feature/test".to_string(),
122 created_at: Utc::now(),
123 tasks: vec![
124 Task {
125 id: "task-1".to_string(),
126 title: "Task 1".to_string(),
127 description: "First task".to_string(),
128 acceptance_criteria: vec![],
129 status: TaskStatus::InProgress,
130 blocked_reason: None,
131 completed_at: None,
132 },
133 Task {
134 id: "task-2".to_string(),
135 title: "Task 2".to_string(),
136 description: "Second task".to_string(),
137 acceptance_criteria: vec![],
138 status: TaskStatus::Pending,
139 blocked_reason: None,
140 completed_at: None,
141 },
142 ],
143 learnings: vec![],
144 };
145
146 let next = spec.find_next_task();
147 assert!(next.is_some());
148 assert_eq!(next.unwrap().id, "task-2");
149}
150
151#[test]
152fn test_find_next_task_returns_none_when_all_complete() {
153 let spec = Spec {
154 name: "test".to_string(),
155 description: "Test".to_string(),
156 branch_name: "feature/test".to_string(),
157 created_at: Utc::now(),
158 tasks: vec![Task {
159 id: "task-1".to_string(),
160 title: "Task 1".to_string(),
161 description: "First task".to_string(),
162 acceptance_criteria: vec![],
163 status: TaskStatus::Complete,
164 blocked_reason: None,
165 completed_at: Some(Utc::now()),
166 }],
167 learnings: vec![],
168 };
169
170 let next = spec.find_next_task();
171 assert!(next.is_none());
172}
173
174#[test]
175fn test_add_learning() {
176 let mut spec = Spec {
177 name: "test".to_string(),
178 description: "Test".to_string(),
179 branch_name: "feature/test".to_string(),
180 created_at: Utc::now(),
181 tasks: vec![],
182 learnings: vec![],
183 };
184
185 spec.add_learning("First learning".to_string());
186 spec.add_learning("Second learning".to_string());
187
188 assert_eq!(spec.learnings.len(), 2);
189 assert_eq!(spec.learnings[0], "First learning");
190 assert_eq!(spec.learnings[1], "Second learning");
191}