Files
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

4.7 KiB

Task 7 — check_exercise response (Backend injection, structured validation errors)

Index: README. Spec: design.

Goal

Pure response function for check_exercise. Distinguishes:

  • Protocol error (unknown exercise id) → { "protocol_error": "..." } so the wrapper can map it to McpError.
  • Validation error (circuit invalid) → { "passed": false, "diagnostics": [...] } per spec §2.2 — never escalates to JSON-RPC error.
  • Normal pass/fail{ "passed": ..., "exercise_id", "feedback", "counts", ... }.

The caller (Task 10) is responsible for persisting mark_solved on passed: true.

Prerequisites

  • Task 5 merged (ExerciseChecker).
  • Task 6 merged (tutor_tools.rs exists).

Files

  • Modify: src/tools/tutor_tools.rs

Steps

  • Step 1: Append failing tests to the existing #[cfg(test)] block
    use crate::executor::{Backend, LocalSimulator};

    fn backend() -> LocalSimulator { LocalSimulator::new() }

    #[test]
    fn check_exercise_x_gate_passes_1_1_a() {
        let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;";
        let resp = check_exercise_response(
            &loader(), &backend() as &dyn Backend, &UserProgress::default(),
            "1-1-a", circuit,
        );
        assert_eq!(resp["passed"], true);
        assert_eq!(resp["exercise_id"], "1-1-a");
    }

    #[test]
    fn check_exercise_identity_fails_1_1_a() {
        let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nc = measure q;";
        let resp = check_exercise_response(
            &loader(), &backend() as &dyn Backend, &UserProgress::default(),
            "1-1-a", circuit,
        );
        assert_eq!(resp["passed"], false);
        assert!(resp["hint"].as_str().is_some());
    }

    #[test]
    fn check_exercise_invalid_circuit_returns_diagnostics_not_protocol_error() {
        let resp = check_exercise_response(
            &loader(), &backend() as &dyn Backend, &UserProgress::default(),
            "1-1-a", "not valid qasm",
        );
        assert_eq!(resp["passed"], false);
        assert!(resp["diagnostics"].as_array().is_some());
        assert!(resp.get("protocol_error").is_none());
    }

    #[test]
    fn check_exercise_unknown_id_marks_protocol_error() {
        let resp = check_exercise_response(
            &loader(), &backend() as &dyn Backend, &UserProgress::default(),
            "99-99-z", "x q[0];",
        );
        assert!(resp["protocol_error"].as_str().is_some());
    }
  • Step 2: Implementation (add at the top-level of tutor_tools.rs)
use crate::executor::Backend;
use crate::tutor::ExerciseChecker;

pub fn check_exercise_response(
    loader: &CurriculumLoader,
    backend: &dyn Backend,
    progress: &UserProgress,
    exercise_id: &str,
    circuit: &str,
) -> Value {
    let exercise = match loader.find_exercise(exercise_id) {
        None => return json!({
            "protocol_error": format!("exercise '{}' not found in curriculum", exercise_id),
        }),
        Some(e) => e,
    };

    let check = ExerciseChecker::check_circuit(backend, circuit, &exercise.criteria);

    if let Some(err) = &check.error {
        let diagnostics: Vec<Value> = check
            .diagnostics
            .iter()
            .map(|d| json!({
                "line": d.line,
                "column": d.column,
                "message": d.message,
            }))
            .collect();
        return json!({
            "passed": false,
            "exercise_id": exercise_id,
            "feedback": exercise.feedback_fail,
            "hint": exercise.hint,
            "validation_error": err,
            "diagnostics": diagnostics,
            "counts": check.counts,
        });
    }

    let already_solved = progress.has_solved(exercise_id);
    if check.passed {
        json!({
            "passed": true,
            "exercise_id": exercise_id,
            "feedback": exercise.feedback_pass,
            "counts": check.counts,
            "newly_solved": !already_solved,
        })
    } else {
        json!({
            "passed": false,
            "exercise_id": exercise_id,
            "feedback": exercise.feedback_fail,
            "hint": exercise.hint,
            "counts": check.counts,
        })
    }
}
  • Step 3: Run tests
cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED"

Expected: test result: ok. 8 passed.

  • Step 4: Commit
git add src/tools/tutor_tools.rs
git commit -m "feat: check_exercise response with backend injection and structured validation errors"