Initial import
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# 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"
|
||||
```
|
||||
Reference in New Issue
Block a user