#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckVerdict {
Pass,
Fail(String),
Bonus(String),
}
impl CheckVerdict {
pub fn is_pass(&self) -> bool {
matches!(self, Self::Pass | Self::Bonus(_))
}
}
#[derive(Debug, Clone)]
pub struct CheckResult {
pub name: &'static str,
pub verdict: CheckVerdict,
}
#[derive(Debug, Clone)]
pub struct ExerciseReport {
pub exercise_id: String,
pub checks: Vec<CheckResult>,
}
impl ExerciseReport {
pub fn passed(&self) -> bool {
self.checks.iter().all(|c| c.verdict.is_pass())
}
pub fn bonus_count(&self) -> usize {
self.checks
.iter()
.filter(|c| matches!(c.verdict, CheckVerdict::Bonus(_)))
.count()
}
}
#[derive(Debug, Clone)]
pub struct LevelReport {
pub level: u8,
pub exercises: Vec<ExerciseReport>,
}
impl LevelReport {
pub fn all_passed(&self) -> bool {
self.exercises.iter().all(|e| e.passed())
}
pub fn pass_count(&self) -> usize {
self.exercises.iter().filter(|e| e.passed()).count()
}
}
pub fn check_no_todos(yaml: &str) -> CheckResult {
let has_todos = yaml.lines().any(|line| {
let trimmed = line.trim_start();
if trimmed.starts_with('#') {
return false;
}
trimmed.contains("TODO") || trimmed.contains("FIXME") || trimmed.contains("XXX")
});
CheckResult {
name: "no_todos",
verdict: if has_todos {
CheckVerdict::Fail("Workflow still contains TODO/FIXME/XXX placeholders".into())
} else {
CheckVerdict::Pass
},
}
}
pub fn check_has_verb(yaml: &str, verb: &str) -> CheckResult {
let pattern = format!("{}:", verb);
CheckResult {
name: "has_verb",
verdict: if yaml.contains(&pattern) {
CheckVerdict::Pass
} else {
CheckVerdict::Fail(format!("Expected verb '{}:' not found", verb))
},
}
}
pub fn check_has_depends_on(yaml: &str) -> CheckResult {
CheckResult {
name: "has_depends_on",
verdict: if yaml.contains("depends_on:") || yaml.contains("depends_on: [") {
CheckVerdict::Pass
} else {
CheckVerdict::Fail("No depends_on: found — tasks should declare dependencies".into())
},
}
}
pub fn check_has_for_each(yaml: &str) -> CheckResult {
CheckResult {
name: "has_for_each",
verdict: if yaml.contains("for_each:") {
CheckVerdict::Pass
} else {
CheckVerdict::Fail("No for_each: found — expected parallel iteration".into())
},
}
}
pub fn check_has_with_bindings(yaml: &str) -> CheckResult {
let has_with = yaml.contains("with:")
&& (yaml.contains(": $") || yaml.contains(": \"$") || yaml.contains(": '$"));
CheckResult {
name: "has_with_bindings",
verdict: if has_with {
CheckVerdict::Pass
} else {
CheckVerdict::Fail("No with: bindings found — use with: { alias: $task_id }".into())
},
}
}
pub fn check_has_schema(yaml: &str) -> CheckResult {
CheckResult {
name: "has_schema",
verdict: if yaml.contains("schema:") && yaml.contains("nika/workflow@0.12") {
CheckVerdict::Pass
} else {
CheckVerdict::Fail("Missing schema declaration: schema: \"nika/workflow@0.12\"".into())
},
}
}
pub fn check_min_tasks(yaml: &str, min: usize) -> CheckResult {
let count = yaml.matches("- id:").count();
CheckResult {
name: "min_tasks",
verdict: if count >= min {
CheckVerdict::Pass
} else {
CheckVerdict::Fail(format!(
"Expected at least {} tasks but found {}",
min, count
))
},
}
}
pub fn bonus_no_hints_used(hints_used: u32) -> CheckResult {
CheckResult {
name: "bonus_no_hints",
verdict: if hints_used == 0 {
CheckVerdict::Bonus("Completed without hints!".into())
} else {
CheckVerdict::Pass },
}
}
pub fn bonus_first_try(is_first_try: bool) -> CheckResult {
CheckResult {
name: "bonus_first_try",
verdict: if is_first_try {
CheckVerdict::Bonus("First try!".into())
} else {
CheckVerdict::Pass },
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_YAML: &str = r#"
schema: "nika/workflow@0.12"
workflow: test
tasks:
- id: step1
exec:
run: echo "hello"
- id: step2
depends_on: [step1]
with:
result: $step1
infer:
prompt: "{{with.result}}"
"#;
#[test]
fn test_check_no_todos_pass() {
let result = check_no_todos(SAMPLE_YAML);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_no_todos_fail() {
let yaml = "tasks:\n - id: a\n exec: \"TODO: fix this\"";
let result = check_no_todos(yaml);
assert!(!result.verdict.is_pass());
}
#[test]
fn test_check_has_verb_found() {
let result = check_has_verb(SAMPLE_YAML, "exec");
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_has_verb_missing() {
let result = check_has_verb(SAMPLE_YAML, "fetch");
assert!(!result.verdict.is_pass());
}
#[test]
fn test_check_has_depends_on() {
let result = check_has_depends_on(SAMPLE_YAML);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_has_depends_on_missing() {
let yaml = "tasks:\n - id: a\n exec:\n run: echo hi";
let result = check_has_depends_on(yaml);
assert!(!result.verdict.is_pass());
}
#[test]
fn test_check_has_for_each() {
let yaml = "tasks:\n - id: a\n for_each: [1, 2]\n exec:\n run: echo hi";
let result = check_has_for_each(yaml);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_has_for_each_missing() {
let result = check_has_for_each(SAMPLE_YAML);
assert!(!result.verdict.is_pass());
}
#[test]
fn test_check_has_with_bindings() {
let result = check_has_with_bindings(SAMPLE_YAML);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_has_schema() {
let result = check_has_schema(SAMPLE_YAML);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_has_schema_missing() {
let yaml = "workflow: test\ntasks:\n - id: a\n exec:\n run: echo hi";
let result = check_has_schema(yaml);
assert!(!result.verdict.is_pass());
}
#[test]
fn test_check_min_tasks_pass() {
let result = check_min_tasks(SAMPLE_YAML, 2);
assert!(result.verdict.is_pass());
}
#[test]
fn test_check_min_tasks_fail() {
let result = check_min_tasks(SAMPLE_YAML, 5);
assert!(!result.verdict.is_pass());
}
#[test]
fn test_bonus_no_hints() {
let result = bonus_no_hints_used(0);
assert!(matches!(result.verdict, CheckVerdict::Bonus(_)));
}
#[test]
fn test_bonus_no_hints_used_some() {
let result = bonus_no_hints_used(3);
assert_eq!(result.verdict, CheckVerdict::Pass);
}
#[test]
fn test_bonus_first_try() {
let result = bonus_first_try(true);
assert!(matches!(result.verdict, CheckVerdict::Bonus(_)));
}
#[test]
fn test_bonus_first_try_not() {
let result = bonus_first_try(false);
assert_eq!(result.verdict, CheckVerdict::Pass);
}
#[test]
fn test_exercise_report_passed() {
let report = ExerciseReport {
exercise_id: "01-01".into(),
checks: vec![
check_has_schema(SAMPLE_YAML),
check_has_verb(SAMPLE_YAML, "exec"),
check_min_tasks(SAMPLE_YAML, 2),
],
};
assert!(report.passed());
}
#[test]
fn test_exercise_report_failed() {
let report = ExerciseReport {
exercise_id: "01-01".into(),
checks: vec![
check_has_schema(SAMPLE_YAML),
check_has_verb(SAMPLE_YAML, "agent"), ],
};
assert!(!report.passed());
}
#[test]
fn test_exercise_report_bonus_count() {
let report = ExerciseReport {
exercise_id: "01-01".into(),
checks: vec![
check_has_schema(SAMPLE_YAML),
bonus_no_hints_used(0),
bonus_first_try(true),
],
};
assert_eq!(report.bonus_count(), 2);
}
#[test]
fn test_level_report() {
let report = LevelReport {
level: 1,
exercises: vec![
ExerciseReport {
exercise_id: "01-01".into(),
checks: vec![check_has_schema(SAMPLE_YAML)],
},
ExerciseReport {
exercise_id: "01-02".into(),
checks: vec![check_has_schema(SAMPLE_YAML)],
},
],
};
assert!(report.all_passed());
assert_eq!(report.pass_count(), 2);
}
#[test]
fn test_check_verdict_is_pass() {
assert!(CheckVerdict::Pass.is_pass());
assert!(CheckVerdict::Bonus("nice".into()).is_pass());
assert!(!CheckVerdict::Fail("bad".into()).is_pass());
}
#[test]
fn test_check_no_todos_ignores_comments() {
let yaml = "schema: \"nika/workflow@0.12\"\nworkflow: test\n# TODO: This is a guide comment\ntasks:\n - id: hello\n exec: \"echo hello\"\n";
let result = check_no_todos(yaml);
assert!(result.verdict.is_pass(), "TODO in comments should not fail");
}
#[test]
fn test_check_no_todos_catches_inline_todos() {
let yaml = "schema: \"nika/workflow@0.12\"\ntasks:\n - id: hello\n exec: \"TODO implement this\"\n";
let result = check_no_todos(yaml);
assert!(!result.verdict.is_pass(), "TODO in values should fail");
}
}