mirror of
https://github.com/saymrwulf/QuantumLearning.git
synced 2026-06-30 03:38:06 +00:00
Add workflow validation and self-service operations
This commit is contained in:
parent
773ec2ba21
commit
9fe3ed6b22
12 changed files with 702 additions and 43 deletions
36
FIRST_RUN.md
36
FIRST_RUN.md
|
|
@ -4,42 +4,55 @@ Use this the first time you open the platform, or anytime you want a clean local
|
|||
|
||||
## Checklist
|
||||
|
||||
1. Start the repo-local server:
|
||||
1. If this is a fresh Mac or a fresh clone, bootstrap the repo first:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
```
|
||||
|
||||
2. Check the local state:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
```
|
||||
|
||||
3. Start the repo-local server:
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh
|
||||
```
|
||||
|
||||
2. Open the localhost URL printed by the script.
|
||||
3. Confirm the kernel in the top-right corner is `QuantumLearning (.venv)`.
|
||||
4. Run `Kernel -> Restart Kernel and Run All Cells...` inside `notebooks/COURSE_BLUEPRINT.ipynb`.
|
||||
5. Read the rebuilt `Foundations` band in this order:
|
||||
4. Open the localhost URL printed by the script.
|
||||
5. Confirm the kernel in the top-right corner is `QuantumLearning (.venv)`.
|
||||
6. Run `Kernel -> Restart Kernel and Run All Cells...` inside `notebooks/COURSE_BLUEPRINT.ipynb`.
|
||||
7. Read the rebuilt `Foundations` band in this order:
|
||||
`module_01_principles_and_circuit_literacy`
|
||||
`module_02_qubit_and_statevector_intuition`
|
||||
`module_03_gates_and_measurement`
|
||||
and inside each module use:
|
||||
`lecture.ipynb -> lab.ipynb -> problems.ipynb -> studio.ipynb`.
|
||||
6. Continue into the rebuilt `Qiskit Engineering` band:
|
||||
8. Continue into the rebuilt `Qiskit Engineering` band:
|
||||
`module_01_circuit_construction_and_analysis`
|
||||
`module_02_transpilation_and_visualization`
|
||||
`module_03_simulation_and_noise_models`
|
||||
again using:
|
||||
`lecture.ipynb -> lab.ipynb -> problems.ipynb -> studio.ipynb`.
|
||||
7. Continue into the rebuilt `Algorithmic Design` band:
|
||||
9. Continue into the rebuilt `Algorithmic Design` band:
|
||||
`module_01_deutsch_family`
|
||||
`module_02_bernstein_vazirani`
|
||||
`module_03_qft`
|
||||
`module_04_grover`
|
||||
again using:
|
||||
`lecture.ipynb -> lab.ipynb -> problems.ipynb -> studio.ipynb`.
|
||||
8. Continue into the rebuilt `Professional Design` band:
|
||||
10. Continue into the rebuilt `Professional Design` band:
|
||||
`module_01_qiskit_patterns`
|
||||
`module_02_hardware_aware_redesign`
|
||||
`module_03_noise_aware_verification`
|
||||
`module_04_capstone_design_review`
|
||||
again using:
|
||||
`lecture.ipynb -> lab.ipynb -> problems.ipynb -> studio.ipynb`.
|
||||
9. Use `notebooks/PROFESSIONAL_PATH.ipynb` when you want the full mastery-model context.
|
||||
11. Use `notebooks/PROFESSIONAL_PATH.ipynb` when you want the full mastery-model context.
|
||||
|
||||
## Expected Local Behavior
|
||||
|
||||
|
|
@ -54,6 +67,7 @@ Use this the first time you open the platform, or anytime you want a clean local
|
|||
If you want a quick terminal check before opening the browser:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pytest -q
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
```
|
||||
|
||||
For the full operational guide, see [OPERATIONS.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/OPERATIONS.md).
|
||||
|
|
|
|||
175
OPERATIONS.md
Normal file
175
OPERATIONS.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Operations
|
||||
|
||||
This file is the self-service operations manual for `QuantumLearning`. It is written to match the repo as it actually exists now, not a hypothetical future state.
|
||||
|
||||
## Operating Model
|
||||
|
||||
The project is intentionally local-first and repo-local:
|
||||
|
||||
- Python environment lives in `.venv/`
|
||||
- Jupyter config and runtime live in `.jupyter_config/`, `.jupyter_data/`, `.jupyter_runtime/`, `.ipython/`, and `.cache/matplotlib/`
|
||||
- Playwright browser runtime lives in `.playwright-browsers/`
|
||||
- validation artifacts live in `.tmp_test_artifacts/`
|
||||
|
||||
That means the day-to-day operational commands are all inside this repo.
|
||||
|
||||
## Bootstrap On A New Mac
|
||||
|
||||
Prerequisites:
|
||||
|
||||
1. macOS
|
||||
2. Python `3.12`
|
||||
3. internet access for the initial dependency and browser download
|
||||
|
||||
If `python3.12` is missing, install it first.
|
||||
Typical options:
|
||||
|
||||
```bash
|
||||
brew install python@3.12
|
||||
```
|
||||
|
||||
or install Python `3.12` from `python.org`.
|
||||
|
||||
Then bootstrap the repo:
|
||||
|
||||
```bash
|
||||
cd /path/to/QuantumLearning
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
```
|
||||
|
||||
That script will:
|
||||
|
||||
- create `.venv/` if needed
|
||||
- install the Python dependencies from `pyproject.toml`
|
||||
- install the project-local Playwright Chromium runtime into `.playwright-browsers/`
|
||||
|
||||
If you want the bootstrap to finish with a full validation run:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh --run-validation
|
||||
```
|
||||
|
||||
If you want to delay the browser download:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh --skip-browser
|
||||
```
|
||||
|
||||
## Start And Use The Course
|
||||
|
||||
Start JupyterLab:
|
||||
|
||||
```bash
|
||||
./scripts/start_jupyter.sh
|
||||
```
|
||||
|
||||
Open the exact localhost URL printed by the script. Do not guess the port; Jupyter will automatically move to the next free port if the requested one is already occupied.
|
||||
|
||||
Use the course in this order:
|
||||
|
||||
1. `notebooks/COURSE_BLUEPRINT.ipynb`
|
||||
2. each module as `lecture -> lab -> problems -> studio`
|
||||
|
||||
## Monitoring The Project
|
||||
|
||||
Use the repo-native status script:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
```
|
||||
|
||||
It reports:
|
||||
|
||||
- current git branch and commit
|
||||
- whether the worktree is clean or dirty
|
||||
- whether `.venv/` exists and which Python version it uses
|
||||
- whether the project-local Playwright browser runtime is installed
|
||||
- whether a repo-local Jupyter server is running and, if so, its full URL
|
||||
|
||||
If you lose the Jupyter URL, `bash ./scripts/project_status.sh` is the fastest recovery path.
|
||||
|
||||
In restricted automation sandboxes, the script may report that the live process probe is unavailable. In a normal terminal on your Mac, it will report the running server directly when the runtime file and process are both visible.
|
||||
|
||||
## Validation Modes
|
||||
|
||||
Quick local validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
- `ruff check src tests scripts`
|
||||
- `pytest -m "not browser and not notebook"`
|
||||
|
||||
Standard validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --standard
|
||||
```
|
||||
|
||||
This runs the full pytest suite without coverage reporting.
|
||||
|
||||
Release-style validation:
|
||||
|
||||
```bash
|
||||
bash ./scripts/run_validation.sh --full
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
- `ruff check src tests scripts`
|
||||
- full pytest suite
|
||||
- coverage reporting
|
||||
|
||||
In restricted automation sandboxes, the browser and notebook execution suites may skip because localhost kernel and browser ports are blocked. In a normal terminal on your Mac, the same command should run those suites instead of skipping them.
|
||||
|
||||
At the current state of the repo, the full validation suite includes:
|
||||
|
||||
- curriculum/config/runtime tests
|
||||
- notebook pedagogy tests
|
||||
- notebook execution tests
|
||||
- real JupyterLab browser workflow validation
|
||||
- assessment and capstone validation tests
|
||||
|
||||
## What The Browser Workflow Validation Now Covers
|
||||
|
||||
The browser suite no longer stops at “page opened.”
|
||||
|
||||
It now validates learner-style flows such as:
|
||||
|
||||
- opening notebooks through JupyterLab
|
||||
- running all notebook cells
|
||||
- answering a multiple-choice quiz
|
||||
- editing an `editable_circuit_lab` code cell and re-rendering it
|
||||
- completing an evidence checklist
|
||||
- filling a feedback iteration panel
|
||||
- moving rubric sliders and observing the grading summary update
|
||||
|
||||
## Recovery Checklist
|
||||
|
||||
If something feels wrong, use this order:
|
||||
|
||||
1. `bash ./scripts/project_status.sh`
|
||||
2. `bash ./scripts/run_validation.sh --quick`
|
||||
3. if needed, `bash ./scripts/run_validation.sh --full`
|
||||
4. restart Jupyter with `./scripts/start_jupyter.sh`
|
||||
|
||||
If `.venv/` is missing or broken, re-run:
|
||||
|
||||
```bash
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
```
|
||||
|
||||
## Another-Mac Transfer Pattern
|
||||
|
||||
The clean transfer model is:
|
||||
|
||||
1. clone or copy the repo to the new Mac
|
||||
2. make sure Python `3.12` exists
|
||||
3. run `bash ./scripts/bootstrap_mac.sh`
|
||||
4. run `bash ./scripts/run_validation.sh --quick`
|
||||
5. run `./scripts/start_jupyter.sh`
|
||||
|
||||
That is the intended self-service installation path. You do not need agent support for normal setup, validation, status checks, or Jupyter startup.
|
||||
30
README.md
30
README.md
|
|
@ -31,11 +31,13 @@ The current rebuild direction is now explicit:
|
|||
|
||||
```text
|
||||
QuantumLearning/
|
||||
├── scripts/
|
||||
├── assets/figures/
|
||||
├── configs/
|
||||
├── notebooks/
|
||||
├── src/
|
||||
├── tests/
|
||||
├── .playwright-browsers/
|
||||
├── .venv/
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
|
|
@ -43,14 +45,20 @@ QuantumLearning/
|
|||
|
||||
## Quick Start
|
||||
|
||||
Use the repo-local Python 3.12 environment.
|
||||
Use the repo-local bootstrap script on macOS:
|
||||
|
||||
```bash
|
||||
cd /Users/oho/GitClone/CodexProjects/QuantumLearning
|
||||
python3.12 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
bash ./scripts/bootstrap_mac.sh
|
||||
```
|
||||
|
||||
That script creates `.venv/`, installs the Python dependencies, and installs the project-local Playwright Chromium runtime into `.playwright-browsers/`.
|
||||
|
||||
If you want a fast operational check immediately after bootstrap:
|
||||
|
||||
```bash
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
```
|
||||
|
||||
Launch Jupyter from the project root with the repo-local wrapper so the notebooks, kernel, and local config stay aligned.
|
||||
|
|
@ -70,18 +78,18 @@ If you want a different local port:
|
|||
Run the tests:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pytest
|
||||
bash ./scripts/run_validation.sh --standard
|
||||
```
|
||||
|
||||
Run the measured local coverage report:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
pytest --cov=quantum_learning --cov-report=term-missing
|
||||
bash ./scripts/run_validation.sh --full
|
||||
```
|
||||
|
||||
Install the project-local browser runtime used by the real JupyterLab UX tests:
|
||||
On a normal Mac terminal, the full validation path runs the browser and notebook execution suites as well. In restricted automation sandboxes, those suites may skip because localhost kernel or browser ports are blocked.
|
||||
|
||||
If you deliberately skipped the local browser runtime during bootstrap, install it later with:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
|
|
@ -132,6 +140,7 @@ The `Professional Design` band is now rebuilt as well:
|
|||
The older single-notebook sequence is still present as legacy reference material, but the canonical course path is now the full bundle architecture across all four technical bands.
|
||||
|
||||
See [FIRST_RUN.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/FIRST_RUN.md) for the short startup checklist.
|
||||
See [OPERATIONS.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/OPERATIONS.md) for setup on another Mac, monitoring, validation, and recovery.
|
||||
|
||||
## Local-First Design Rules
|
||||
|
||||
|
|
@ -139,3 +148,4 @@ See [FIRST_RUN.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/FIRST_RUN.m
|
|||
- No IBM token is required.
|
||||
- Transpilation examples use local constraints and simulator-compatible backends.
|
||||
- Circuit graphics are first-class and rely on local `matplotlib`, `pylatexenc`, and `Pillow`.
|
||||
- The browser validation stack uses a repo-local Playwright runtime in `.playwright-browsers/`.
|
||||
|
|
|
|||
96
scripts/bootstrap_mac.sh
Executable file
96
scripts/bootstrap_mac.sh
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
INSTALL_BROWSER=1
|
||||
RUN_VALIDATION=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash ./scripts/bootstrap_mac.sh [--skip-browser] [--run-validation]
|
||||
|
||||
Bootstrap QuantumLearning on a Mac using only repo-local runtime paths.
|
||||
|
||||
Options:
|
||||
--skip-browser Skip installing the project-local Playwright Chromium runtime.
|
||||
--run-validation Run the full validation suite after bootstrapping.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--skip-browser)
|
||||
INSTALL_BROWSER=0
|
||||
shift
|
||||
;;
|
||||
--run-validation)
|
||||
RUN_VALIDATION=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
resolve_python() {
|
||||
if command -v python3.12 >/dev/null 2>&1; then
|
||||
echo "python3.12"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
if python3 -c 'import sys; raise SystemExit(0 if sys.version_info[:2] == (3, 12) else 1)'; then
|
||||
echo "python3"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! PYTHON_BIN="$(resolve_python)"; then
|
||||
cat <<'EOF' >&2
|
||||
Python 3.12 is required.
|
||||
|
||||
Install it first, then re-run this script.
|
||||
Typical options on macOS:
|
||||
brew install python@3.12
|
||||
or install Python 3.12 from python.org
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if [[ ! -d "$ROOT_DIR/.venv" ]]; then
|
||||
"$PYTHON_BIN" -m venv "$ROOT_DIR/.venv"
|
||||
fi
|
||||
|
||||
source "$ROOT_DIR/.venv/bin/activate"
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
|
||||
if [[ "$INSTALL_BROWSER" -eq 1 ]]; then
|
||||
PLAYWRIGHT_BROWSERS_PATH="$ROOT_DIR/.playwright-browsers" python -m playwright install chromium
|
||||
fi
|
||||
|
||||
if [[ "$RUN_VALIDATION" -eq 1 ]]; then
|
||||
bash "$ROOT_DIR/scripts/run_validation.sh"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
Bootstrap complete.
|
||||
|
||||
Next steps:
|
||||
bash ./scripts/project_status.sh
|
||||
bash ./scripts/run_validation.sh --quick
|
||||
./scripts/start_jupyter.sh
|
||||
EOF
|
||||
0
scripts/normalize_notebook_ids.py
Normal file → Executable file
0
scripts/normalize_notebook_ids.py
Normal file → Executable file
81
scripts/project_status.sh
Executable file
81
scripts/project_status.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash ./scripts/project_status.sh
|
||||
|
||||
Print the current local operational state of the QuantumLearning repo:
|
||||
git state, repo-local Python environment, Playwright browser runtime, and
|
||||
the latest repo-local Jupyter server if one is running.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
BRANCH="$(git branch --show-current)"
|
||||
COMMIT="$(git rev-parse --short HEAD)"
|
||||
if [[ -n "$(git status --short)" ]]; then
|
||||
GIT_STATE="dirty"
|
||||
else
|
||||
GIT_STATE="clean"
|
||||
fi
|
||||
|
||||
echo "Root: $ROOT_DIR"
|
||||
echo "Git: $BRANCH @ $COMMIT ($GIT_STATE)"
|
||||
|
||||
if [[ -x "$ROOT_DIR/.venv/bin/python" ]]; then
|
||||
PYTHON_VERSION="$("$ROOT_DIR/.venv/bin/python" -c 'import sys; print(sys.version.split()[0])')"
|
||||
echo "Venv: present (Python $PYTHON_VERSION)"
|
||||
else
|
||||
echo "Venv: missing"
|
||||
fi
|
||||
|
||||
if compgen -G "$ROOT_DIR/.playwright-browsers/chromium-*" >/dev/null; then
|
||||
echo "Browser runtime: installed"
|
||||
else
|
||||
echo "Browser runtime: missing"
|
||||
fi
|
||||
|
||||
if compgen -G "$ROOT_DIR/.jupyter_runtime/jpserver-*.json" >/dev/null; then
|
||||
LATEST_SERVER_JSON="$(ls -t "$ROOT_DIR"/.jupyter_runtime/jpserver-*.json | head -n 1)"
|
||||
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$LATEST_SERVER_JSON"
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
data = json.loads(path.read_text())
|
||||
pid = int(data["pid"])
|
||||
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
print("Jupyter: runtime file exists but process is not running")
|
||||
print(f" Runtime file: {path}")
|
||||
except PermissionError:
|
||||
print("Jupyter: runtime file present; live process probe is unavailable in this environment")
|
||||
print(f" URL: {data['url']}lab/tree/notebooks/COURSE_BLUEPRINT.ipynb?token={data['token']}")
|
||||
print(f" Runtime file: {path}")
|
||||
else:
|
||||
print(f"Jupyter: running (pid {pid})")
|
||||
print(f" URL: {data['url']}lab/tree/notebooks/COURSE_BLUEPRINT.ipynb?token={data['token']}")
|
||||
print(f" Runtime file: {path}")
|
||||
PY
|
||||
else
|
||||
echo "Jupyter: no repo-local runtime file found"
|
||||
fi
|
||||
|
||||
echo "Validation:"
|
||||
echo " Quick: bash ./scripts/run_validation.sh --quick"
|
||||
echo " Full: bash ./scripts/run_validation.sh --full"
|
||||
66
scripts/run_validation.sh
Executable file
66
scripts/run_validation.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
MODE="full"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: bash ./scripts/run_validation.sh [--quick|--standard|--full]
|
||||
|
||||
Validation modes:
|
||||
--quick Ruff + pytest without browser/notebook execution suites.
|
||||
--standard Ruff + full pytest suite.
|
||||
--full Ruff + full pytest suite with coverage report. Default.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--quick)
|
||||
MODE="quick"
|
||||
shift
|
||||
;;
|
||||
--standard)
|
||||
MODE="standard"
|
||||
shift
|
||||
;;
|
||||
--full)
|
||||
MODE="full"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
|
||||
echo "Missing repo-local .venv. Run bash ./scripts/bootstrap_mac.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
source "$ROOT_DIR/.venv/bin/activate"
|
||||
export PLAYWRIGHT_BROWSERS_PATH="$ROOT_DIR/.playwright-browsers"
|
||||
|
||||
ruff check src tests scripts
|
||||
|
||||
case "$MODE" in
|
||||
quick)
|
||||
python -m pytest -q -m "not browser and not notebook"
|
||||
;;
|
||||
standard)
|
||||
python -m pytest -q
|
||||
;;
|
||||
full)
|
||||
python -m pytest --cov=quantum_learning --cov-report=term-missing -q
|
||||
;;
|
||||
esac
|
||||
|
|
@ -87,3 +87,22 @@ def test_capstone_rubric_maps_to_intended_competence_dimensions():
|
|||
"recommendation",
|
||||
"risk",
|
||||
]
|
||||
|
||||
|
||||
def test_capstone_rubric_rewards_complete_design_review_evidence():
|
||||
result = score_named_rubric(
|
||||
"capstone_design_review",
|
||||
{
|
||||
"brief": 4,
|
||||
"candidates": 4,
|
||||
"ideal": 4,
|
||||
"hardware": 4,
|
||||
"noise": 4,
|
||||
"recommendation": 4,
|
||||
"risk": 4,
|
||||
},
|
||||
)
|
||||
|
||||
assert result.total_score == 28
|
||||
assert result.passed
|
||||
assert result.band == "mastery-ready"
|
||||
|
|
|
|||
|
|
@ -53,6 +53,62 @@ def _wait_for_notebook(browser_page, notebook_filename: str, heading: str) -> No
|
|||
)
|
||||
|
||||
|
||||
def _open_notebook_from_directory(
|
||||
browser_page,
|
||||
jupyter_lab_server,
|
||||
directory: str,
|
||||
notebook_filename: str,
|
||||
heading: str,
|
||||
) -> None:
|
||||
url = f"{jupyter_lab_server['base_url']}/lab/tree/{directory}?token={jupyter_lab_server['token']}"
|
||||
browser_page.goto(url, wait_until="networkidle")
|
||||
browser_page.locator(".jp-DirListing-itemText", has_text=notebook_filename).first.wait_for(
|
||||
timeout=60000
|
||||
)
|
||||
browser_page.locator(".jp-DirListing-itemText", has_text=notebook_filename).first.dblclick()
|
||||
_wait_for_notebook(browser_page, notebook_filename, heading)
|
||||
|
||||
|
||||
def _run_all_cells(browser_page) -> None:
|
||||
browser_page.locator("text=Run").first.click()
|
||||
browser_page.locator("text=Run All Cells").first.click()
|
||||
|
||||
try:
|
||||
browser_page.get_by_role("button", name="Select").click(timeout=5000)
|
||||
except PlaywrightTimeoutError:
|
||||
pass
|
||||
|
||||
|
||||
def _find_code_editor(browser_page):
|
||||
textareas = browser_page.locator("textarea")
|
||||
for index in range(textareas.count()):
|
||||
candidate = textareas.nth(index)
|
||||
if not candidate.is_visible():
|
||||
continue
|
||||
try:
|
||||
value = candidate.input_value()
|
||||
except PlaywrightTimeoutError: # pragma: no cover - widget race
|
||||
continue
|
||||
if "QuantumCircuit" in value:
|
||||
return candidate
|
||||
raise AssertionError("Could not locate an editable circuit code editor.")
|
||||
|
||||
|
||||
def _wait_for_text_in_body(browser_page, expected_text: str, *, timeout_seconds: int = 120) -> None:
|
||||
deadline = time.time() + timeout_seconds
|
||||
last_body = ""
|
||||
while time.time() < deadline:
|
||||
last_body = browser_page.locator("body").inner_text()
|
||||
if expected_text in last_body:
|
||||
return
|
||||
time.sleep(1)
|
||||
|
||||
raise AssertionError(
|
||||
f"Did not find {expected_text!r} in rendered page text.\nLast body text excerpt:\n"
|
||||
f"{last_body[-4000:]}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def jupyter_lab_server():
|
||||
if sync_playwright is None:
|
||||
|
|
@ -141,31 +197,68 @@ def test_course_blueprint_loads_in_jupyterlab(browser_page, jupyter_lab_server):
|
|||
|
||||
|
||||
@pytest.mark.browser
|
||||
def test_capstone_studio_runs_and_renders_controls(browser_page, jupyter_lab_server):
|
||||
url = (
|
||||
f"{jupyter_lab_server['base_url']}/lab/tree/notebooks/professional/"
|
||||
"module_04_capstone_design_review"
|
||||
f"?token={jupyter_lab_server['token']}"
|
||||
)
|
||||
browser_page.goto(url, wait_until="networkidle")
|
||||
browser_page.locator(".jp-DirListing-itemText", has_text="studio.ipynb").first.wait_for(
|
||||
timeout=60000
|
||||
)
|
||||
browser_page.locator(".jp-DirListing-itemText", has_text="studio.ipynb").first.dblclick()
|
||||
_wait_for_notebook(
|
||||
def test_foundations_lab_supports_quiz_answering_and_circuit_editing(
|
||||
browser_page,
|
||||
jupyter_lab_server,
|
||||
):
|
||||
_open_notebook_from_directory(
|
||||
browser_page,
|
||||
jupyter_lab_server,
|
||||
"notebooks/foundations/module_01_principles_and_circuit_literacy",
|
||||
"lab.ipynb",
|
||||
"Principles and Circuit Literacy Lab",
|
||||
)
|
||||
_run_all_cells(browser_page)
|
||||
|
||||
browser_page.get_by_label("A. Only one deterministic outcome from the all-zero input").check()
|
||||
browser_page.get_by_role("button", name="Check answer").first.click()
|
||||
browser_page.locator("text=Correct.").first.wait_for(timeout=120000)
|
||||
|
||||
editor = _find_code_editor(browser_page)
|
||||
editor.fill(
|
||||
'circuit = QuantumCircuit(1, 1, name="workflow")\n'
|
||||
"circuit.x(0)\n"
|
||||
"circuit.measure(0, 0)\n"
|
||||
)
|
||||
browser_page.get_by_role("button", name="Render circuit").first.click()
|
||||
browser_page.locator("pre", has_text="{'1': 256}").first.wait_for(timeout=120000)
|
||||
|
||||
|
||||
@pytest.mark.browser
|
||||
def test_capstone_studio_supports_checklist_feedback_and_rubric_workflows(
|
||||
browser_page,
|
||||
jupyter_lab_server,
|
||||
):
|
||||
_open_notebook_from_directory(
|
||||
browser_page,
|
||||
jupyter_lab_server,
|
||||
"notebooks/professional/module_04_capstone_design_review",
|
||||
"studio.ipynb",
|
||||
"Capstone Circuit Design Review Studio",
|
||||
)
|
||||
|
||||
browser_page.locator("text=Run").first.click()
|
||||
browser_page.locator("text=Run All Cells").first.click()
|
||||
|
||||
try:
|
||||
browser_page.get_by_role("button", name="Select").click(timeout=5000)
|
||||
except PlaywrightTimeoutError:
|
||||
pass
|
||||
_run_all_cells(browser_page)
|
||||
|
||||
browser_page.get_by_role("button", name="Render circuit").first.wait_for(timeout=120000)
|
||||
browser_page.get_by_role("button", name="Check answer").first.wait_for(timeout=120000)
|
||||
browser_page.locator("text=Evidence Checklist").first.wait_for(timeout=120000)
|
||||
browser_page.locator("text=Capstone Circuit Design Review Evidence Checklist").first.wait_for(
|
||||
timeout=120000
|
||||
)
|
||||
|
||||
browser_page.get_by_placeholder("State the current claim or judgement.").fill(
|
||||
"The middle-root GHZ candidate best satisfies the declared local brief."
|
||||
)
|
||||
browser_page.get_by_placeholder("Name the exact evidence supporting that claim.").fill(
|
||||
"It preserves the ideal target, compiles with lower burden on the line, and retains stronger noisy support."
|
||||
)
|
||||
browser_page.get_by_placeholder("Name the main remaining risk, assumption, or weakness.").fill(
|
||||
"The comparison still depends on a synthetic local noise model."
|
||||
)
|
||||
browser_page.get_by_placeholder("State the next revision, experiment, or rewrite.").fill(
|
||||
"Compare the same candidates under a second line-constrained noise profile."
|
||||
)
|
||||
browser_page.get_by_role("button", name="Review draft").click()
|
||||
browser_page.locator("text=Missing sections: None").first.wait_for(timeout=120000)
|
||||
|
||||
_wait_for_text_in_body(browser_page, "Capstone Circuit Design Review Studio Self-Grading")
|
||||
_wait_for_text_in_body(browser_page, "Brief Clarity")
|
||||
_wait_for_text_in_body(browser_page, "Total: 0/28")
|
||||
_wait_for_text_in_body(browser_page, "Band: revision-needed")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import ipywidgets as widgets
|
||||
|
||||
from quantum_learning import load_assessment_blueprint
|
||||
from qiskit import QuantumCircuit
|
||||
|
||||
|
|
@ -76,6 +78,23 @@ def test_rubric_scorecard_builds_widget():
|
|||
assert widget.children
|
||||
|
||||
|
||||
def test_rubric_scorecard_updates_summary_when_sliders_change():
|
||||
rubric = load_assessment_blueprint().get_rubric("capstone_design_review")
|
||||
widget = rubric_scorecard(rubric, title="Capstone Review")
|
||||
|
||||
sliders = [child for child in widget.children if isinstance(child, widgets.IntSlider)]
|
||||
summary = widget.children[-1]
|
||||
|
||||
assert len(sliders) == 7
|
||||
assert "Total:</b> 0/28" in summary.value
|
||||
|
||||
for slider in sliders:
|
||||
slider.value = slider.max
|
||||
|
||||
assert "Total:</b> 28/28" in summary.value
|
||||
assert "Band:</b> mastery-ready" in summary.value
|
||||
|
||||
|
||||
def test_feedback_iteration_panel_builds_widget():
|
||||
widget = feedback_iteration_panel(
|
||||
title="Revision Loop",
|
||||
|
|
@ -94,3 +113,26 @@ def test_evidence_checklist_builds_widget():
|
|||
title="Checklist",
|
||||
)
|
||||
assert widget.children
|
||||
|
||||
|
||||
def test_evidence_checklist_updates_summary_when_boxes_change():
|
||||
widget = evidence_checklist(
|
||||
[
|
||||
"Objective stated",
|
||||
"Evidence cited",
|
||||
"Risk named",
|
||||
],
|
||||
title="Checklist",
|
||||
)
|
||||
|
||||
checkboxes = [child for child in widget.children if isinstance(child, widgets.Checkbox)]
|
||||
summary = widget.children[-1]
|
||||
|
||||
assert len(checkboxes) == 3
|
||||
assert "Completion:</b> 0%" in summary.value
|
||||
|
||||
for checkbox in checkboxes:
|
||||
checkbox.value = True
|
||||
|
||||
assert "Completion:</b> 100%" in summary.value
|
||||
assert "Ready for review:</b> Yes" in summary.value
|
||||
|
|
|
|||
59
tests/test_operations.py
Normal file
59
tests/test_operations.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
|
||||
from quantum_learning.config import project_root
|
||||
|
||||
|
||||
def test_bootstrap_script_help_works():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/bootstrap_mac.sh", "--help"],
|
||||
cwd=project_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Usage:" in result.stdout
|
||||
|
||||
|
||||
def test_validation_script_help_works():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/run_validation.sh", "--help"],
|
||||
cwd=project_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "--full" in result.stdout
|
||||
|
||||
|
||||
def test_project_status_script_runs():
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/project_status.sh"],
|
||||
cwd=project_root(),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Git:" in result.stdout
|
||||
assert "Validation:" in result.stdout
|
||||
|
||||
|
||||
def test_operations_docs_use_tested_command_forms():
|
||||
root = project_root()
|
||||
expected_commands = [
|
||||
"bash ./scripts/bootstrap_mac.sh",
|
||||
"bash ./scripts/project_status.sh",
|
||||
"bash ./scripts/run_validation.sh --quick",
|
||||
]
|
||||
|
||||
for relative_path in ("README.md", "FIRST_RUN.md", "OPERATIONS.md"):
|
||||
text = (root / relative_path).read_text()
|
||||
for command in expected_commands:
|
||||
assert command in text
|
||||
|
|
@ -79,6 +79,10 @@ def test_first_run_checklist_exists():
|
|||
assert (project_root() / "FIRST_RUN.md").exists()
|
||||
|
||||
|
||||
def test_operations_guide_exists():
|
||||
assert (project_root() / "OPERATIONS.md").exists()
|
||||
|
||||
|
||||
def test_repo_local_jupyter_config_exists():
|
||||
assert (project_root() / ".jupyter_config" / "jupyter_lab_config.py").exists()
|
||||
assert (project_root() / ".jupyter_config" / "jupyter_server_config.py").exists()
|
||||
|
|
|
|||
Loading…
Reference in a new issue