agentzero_tools/skills/
sop.rs1use anyhow::{bail, Context};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct SopStep {
6 pub title: String,
7 pub completed: bool,
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct SopPlan {
12 pub id: String,
13 pub steps: Vec<SopStep>,
14}
15
16pub fn create_plan(id: &str, steps: &[&str]) -> anyhow::Result<SopPlan> {
17 if id.trim().is_empty() {
18 bail!("sop id cannot be empty");
19 }
20 if steps.is_empty() {
21 bail!("sop requires at least one step");
22 }
23
24 let mapped = steps
25 .iter()
26 .map(|title| SopStep {
27 title: (*title).to_string(),
28 completed: false,
29 })
30 .collect::<Vec<_>>();
31
32 Ok(SopPlan {
33 id: id.to_string(),
34 steps: mapped,
35 })
36}
37
38pub fn advance_step(plan: &mut SopPlan, step_index: usize) -> anyhow::Result<()> {
39 let step = plan
40 .steps
41 .get_mut(step_index)
42 .with_context(|| format!("step index {step_index} is out of range"))?;
43
44 if step.completed {
45 bail!("step `{}` is already completed", step.title);
46 }
47
48 step.completed = true;
49 Ok(())
50}
51
52#[cfg(test)]
53mod tests {
54 use super::{advance_step, create_plan};
55
56 #[test]
57 fn advance_step_success_path() {
58 let mut plan = create_plan("deploy", &["build", "ship"]).expect("plan should create");
59 advance_step(&mut plan, 0).expect("advance should succeed");
60 assert!(plan.steps[0].completed);
61 assert!(!plan.steps[1].completed);
62 }
63
64 #[test]
65 fn advance_step_rejects_out_of_range_negative_path() {
66 let mut plan = create_plan("deploy", &["build"]).expect("plan should create");
67 let err = advance_step(&mut plan, 3).expect_err("out of range should fail");
68 assert!(err.to_string().contains("out of range"));
69 }
70}