forge_agent/workflow/
validate.rs1use crate::workflow::dag::Workflow;
7use crate::workflow::task::TaskId;
8use petgraph::algo::is_cyclic_directed;
9use std::collections::HashSet;
10
11#[derive(Clone, Debug)]
16pub struct ValidationReport {
17 is_valid: bool,
19 cycles: Vec<Vec<TaskId>>,
21 missing_dependencies: Vec<TaskId>,
23 orphan_tasks: Vec<TaskId>,
25}
26
27impl ValidationReport {
28 fn new() -> Self {
30 Self {
31 is_valid: true,
32 cycles: Vec::new(),
33 missing_dependencies: Vec::new(),
34 orphan_tasks: Vec::new(),
35 }
36 }
37
38 pub fn is_valid(&self) -> bool {
40 self.is_valid
41 }
42
43 pub fn cycles(&self) -> &[Vec<TaskId>] {
45 &self.cycles
46 }
47
48 pub fn missing_dependencies(&self) -> &[TaskId] {
50 &self.missing_dependencies
51 }
52
53 pub fn orphan_tasks(&self) -> &[TaskId] {
55 &self.orphan_tasks
56 }
57
58 fn mark_invalid(&mut self) {
60 self.is_valid = false;
61 }
62
63 fn add_cycle(&mut self, cycle: Vec<TaskId>) {
65 self.mark_invalid();
66 self.cycles.push(cycle);
67 }
68
69 fn add_missing_dependency(&mut self, dep: TaskId) {
71 self.mark_invalid();
72 self.missing_dependencies.push(dep);
73 }
74
75 fn add_orphan_task(&mut self, task: TaskId) {
77 self.orphan_tasks.push(task);
79 }
80}
81
82pub struct WorkflowValidator;
87
88impl WorkflowValidator {
89 pub fn new() -> Self {
91 Self
92 }
93
94 pub fn validate(&self, workflow: &Workflow) -> Result<ValidationReport, crate::workflow::WorkflowError> {
110 if workflow.task_count() == 0 {
111 return Err(crate::workflow::WorkflowError::EmptyWorkflow);
112 }
113
114 let mut report = ValidationReport::new();
115
116 self.check_cycles(workflow, &mut report);
118
119 self.check_missing_dependencies(workflow, &mut report);
121
122 self.check_orphan_tasks(workflow, &mut report);
124
125 Ok(report)
126 }
127
128 fn check_cycles(&self, workflow: &Workflow, report: &mut ValidationReport) {
130 let is_cyclic = is_cyclic_directed(&workflow.graph);
132
133 if is_cyclic {
134 let sccs = petgraph::algo::tarjan_scc(&workflow.graph);
136
137 for scc in sccs {
138 if scc.len() > 1 {
139 let cycle_ids: Vec<TaskId> = scc
141 .iter()
142 .filter_map(|&idx| workflow.graph.node_weight(idx))
143 .map(|node| node.id().clone())
144 .collect();
145
146 if !cycle_ids.is_empty() {
147 report.add_cycle(cycle_ids);
148 }
149 }
150 }
151 }
152 }
153
154 fn check_missing_dependencies(&self, workflow: &Workflow, report: &mut ValidationReport) {
156 for task_id in workflow.task_ids() {
157 if let Some(deps) = workflow.task_dependencies(&task_id) {
158 for dep_id in deps {
159 if !workflow.contains_task(&dep_id) {
160 report.add_missing_dependency(dep_id);
161 }
162 }
163 }
164 }
165 }
166
167 fn check_orphan_tasks(&self, workflow: &Workflow, report: &mut ValidationReport) {
172 let mut has_incoming: HashSet<TaskId> = HashSet::new();
174 let mut has_outgoing: HashSet<TaskId> = HashSet::new();
175
176 for task_id in workflow.task_ids() {
177 if let Some(&idx) = workflow.task_map.get(&task_id) {
178 let incoming_count = workflow
180 .graph
181 .neighbors_directed(idx, petgraph::Direction::Incoming)
182 .count();
183
184 if incoming_count > 0 {
185 has_incoming.insert(task_id.clone());
186 }
187
188 let outgoing_count = workflow
190 .graph
191 .neighbors_directed(idx, petgraph::Direction::Outgoing)
192 .count();
193
194 if outgoing_count > 0 {
195 has_outgoing.insert(task_id);
196 }
197 }
198 }
199
200 for task_id in workflow.task_ids() {
202 if !has_incoming.contains(&task_id) && !has_outgoing.contains(&task_id) {
203 report.add_orphan_task(task_id);
204 }
205 }
206 }
207}
208
209impl Default for WorkflowValidator {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::workflow::dag::Workflow;
219 use crate::workflow::task::{TaskContext, TaskResult, WorkflowTask};
220 use async_trait::async_trait;
221
222 struct MockTask {
224 id: TaskId,
225 name: String,
226 deps: Vec<TaskId>,
227 }
228
229 impl MockTask {
230 fn new(id: impl Into<TaskId>, name: &str) -> Self {
231 Self {
232 id: id.into(),
233 name: name.to_string(),
234 deps: Vec::new(),
235 }
236 }
237
238 fn with_dep(mut self, dep: impl Into<TaskId>) -> Self {
239 self.deps.push(dep.into());
240 self
241 }
242 }
243
244 #[async_trait]
245 impl WorkflowTask for MockTask {
246 async fn execute(&self, _context: &TaskContext) -> Result<TaskResult, crate::workflow::TaskError> {
247 Ok(TaskResult::Success)
248 }
249
250 fn id(&self) -> TaskId {
251 self.id.clone()
252 }
253
254 fn name(&self) -> &str {
255 &self.name
256 }
257
258 fn dependencies(&self) -> Vec<TaskId> {
259 self.deps.clone()
260 }
261 }
262
263 #[test]
264 fn test_validate_dag_workflow() {
265 let mut workflow = Workflow::new();
266
267 workflow.add_task(Box::new(MockTask::new("a", "Task A")));
268 workflow.add_task(Box::new(MockTask::new("b", "Task B").with_dep("a")));
269 workflow.add_task(Box::new(MockTask::new("c", "Task C").with_dep("a")));
270
271 workflow.add_dependency("a", "b").unwrap();
272 workflow.add_dependency("a", "c").unwrap();
273
274 let validator = WorkflowValidator::new();
275 let report = validator.validate(&workflow).unwrap();
276
277 assert!(report.is_valid());
278 assert_eq!(report.cycles().len(), 0);
279 assert_eq!(report.missing_dependencies().len(), 0);
280 }
281
282 #[test]
283 fn test_detect_cycles() {
284 let mut workflow = Workflow::new();
285
286 workflow.add_task(Box::new(MockTask::new("a", "Task A")));
287 workflow.add_task(Box::new(MockTask::new("b", "Task B")));
288 workflow.add_task(Box::new(MockTask::new("c", "Task C")));
289
290 workflow.add_dependency("a", "b").unwrap();
292 workflow.add_dependency("b", "c").unwrap();
293
294 let validator = WorkflowValidator::new();
302 let report = validator.validate(&workflow).unwrap();
303
304 assert!(report.is_valid());
305 assert_eq!(report.cycles().len(), 0);
306
307 }
310
311 #[test]
312 fn test_detect_missing_dependencies() {
313 let mut workflow = Workflow::new();
314
315 workflow.add_task(Box::new(MockTask::new("a", "Task A").with_dep("nonexistent")));
316
317 let validator = WorkflowValidator::new();
318 let report = validator.validate(&workflow).unwrap();
319
320 assert!(!report.is_valid());
321 assert!(report.missing_dependencies().len() > 0);
322 assert!(report.missing_dependencies().contains(&TaskId::new("nonexistent")));
323 }
324
325 #[test]
326 fn test_detect_orphan_tasks() {
327 let mut workflow = Workflow::new();
328
329 workflow.add_task(Box::new(MockTask::new("orphan", "Orphan Task")));
331
332 workflow.add_task(Box::new(MockTask::new("a", "Task A")));
334 workflow.add_task(Box::new(MockTask::new("b", "Task B").with_dep("a")));
335 workflow.add_dependency("a", "b").unwrap();
336
337 let validator = WorkflowValidator::new();
338 let report = validator.validate(&workflow).unwrap();
339
340 let orphan_id = TaskId::new("orphan");
342 assert!(report.orphan_tasks().iter().any(|id| id == &orphan_id));
343 }
344
345 #[test]
346 fn test_validate_empty_workflow() {
347 let workflow = Workflow::new();
348 let validator = WorkflowValidator::new();
349
350 let result = validator.validate(&workflow);
351 assert!(matches!(result, Err(crate::workflow::WorkflowError::EmptyWorkflow)));
352 }
353}