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

153 lines
4.7 KiB
Markdown

# 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<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**
```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"
```