# Task 7 — `check_exercise` response (Backend injection, structured validation errors) > **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md). ## 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** ```rust 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`)** ```rust 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 = 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** ```bash cargo test tools::tutor_tools::tests 2>&1 | grep -E "test result|FAILED" ``` Expected: `test result: ok. 8 passed`. - [ ] **Step 4: Commit** ```bash git add src/tools/tutor_tools.rs git commit -m "feat: check_exercise response with backend injection and structured validation errors" ```