Harden consumer app lifecycle and docs

This commit is contained in:
saymrwulf 2026-04-15 15:07:33 +02:00
parent 9fe3ed6b22
commit ef778073de
10 changed files with 636 additions and 93 deletions

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ __pycache__/
.DS_Store
.coverage
.coverage.*
.logs/
.playwright-browsers/
.tmp_test_artifacts/
htmlcov/

View file

@ -7,20 +7,20 @@ Use this the first time you open the platform, or anytime you want a clean local
1. If this is a fresh Mac or a fresh clone, bootstrap the repo first:
```bash
bash ./scripts/bootstrap_mac.sh
bash ./scripts/app.sh bootstrap
```
2. Check the local state:
```bash
bash ./scripts/project_status.sh
bash ./scripts/run_validation.sh --quick
bash ./scripts/app.sh status
bash ./scripts/app.sh validate --quick
```
3. Start the repo-local server:
```bash
./scripts/start_jupyter.sh
bash ./scripts/app.sh start --open
```
4. Open the localhost URL printed by the script.
@ -67,7 +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
bash ./scripts/run_validation.sh --quick
bash ./scripts/app.sh validate --quick
```
For the full operational guide, see [OPERATIONS.md](/Users/oho/GitClone/CodexProjects/QuantumLearning/OPERATIONS.md).

View file

@ -34,10 +34,10 @@ Then bootstrap the repo:
```bash
cd /path/to/QuantumLearning
bash ./scripts/bootstrap_mac.sh
bash ./scripts/app.sh bootstrap
```
That script will:
That command will:
- create `.venv/` if needed
- install the Python dependencies from `pyproject.toml`
@ -46,24 +46,45 @@ That script will:
If you want the bootstrap to finish with a full validation run:
```bash
bash ./scripts/bootstrap_mac.sh --run-validation
bash ./scripts/app.sh bootstrap --run-validation
```
If you want to delay the browser download:
```bash
bash ./scripts/bootstrap_mac.sh --skip-browser
bash ./scripts/app.sh bootstrap --skip-browser
```
## Start And Use The Course
Start JupyterLab:
Start the app:
```bash
./scripts/start_jupyter.sh
bash ./scripts/app.sh start --open
```
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.
That starts the repo-local Jupyter server as a detached background service, prints the exact URL, cleans up stale runtime files when needed, and opens it in the default browser.
If you run the launcher from an automation shell that reaps background child processes when the shell exits, use foreground mode instead:
```bash
bash ./scripts/app.sh start --foreground
```
Other normal consumer commands:
```bash
bash ./scripts/app.sh status
bash ./scripts/app.sh restart
bash ./scripts/app.sh stop
bash ./scripts/app.sh logs -f
```
If you want foreground mode for debugging:
```bash
bash ./scripts/app.sh start --foreground
```
Use the course in this order:
@ -75,7 +96,7 @@ Use the course in this order:
Use the repo-native status script:
```bash
bash ./scripts/project_status.sh
bash ./scripts/app.sh status
```
It reports:
@ -86,7 +107,7 @@ It reports:
- 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.
If you lose the Jupyter URL, `bash ./scripts/app.sh status` 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.
@ -95,7 +116,7 @@ In restricted automation sandboxes, the script may report that the live process
Quick local validation:
```bash
bash ./scripts/run_validation.sh --quick
bash ./scripts/app.sh validate --quick
```
This runs:
@ -106,7 +127,7 @@ This runs:
Standard validation:
```bash
bash ./scripts/run_validation.sh --standard
bash ./scripts/app.sh validate --standard
```
This runs the full pytest suite without coverage reporting.
@ -114,7 +135,7 @@ This runs the full pytest suite without coverage reporting.
Release-style validation:
```bash
bash ./scripts/run_validation.sh --full
bash ./scripts/app.sh validate --full
```
This runs:
@ -151,15 +172,15 @@ It now validates learner-style flows such as:
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`
1. `bash ./scripts/app.sh status`
2. `bash ./scripts/app.sh validate --quick`
3. if needed, `bash ./scripts/app.sh validate --full`
4. restart Jupyter with `bash ./scripts/app.sh restart`
If `.venv/` is missing or broken, re-run:
```bash
bash ./scripts/bootstrap_mac.sh
bash ./scripts/app.sh bootstrap
```
## Another-Mac Transfer Pattern
@ -168,8 +189,19 @@ 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`
3. run `bash ./scripts/app.sh bootstrap`
4. run `bash ./scripts/app.sh validate --quick`
5. run `bash ./scripts/app.sh start --open`
That is the intended self-service installation path. You do not need agent support for normal setup, validation, status checks, or Jupyter startup.
## Underlying Plumbing
The lower-level scripts still exist for direct use or debugging:
- `bash ./scripts/bootstrap_mac.sh`
- `bash ./scripts/project_status.sh`
- `bash ./scripts/run_validation.sh --quick`
- `./scripts/start_jupyter.sh --foreground`
Those are implementation details now. The normal consumer interface is `bash ./scripts/app.sh ...`.

View file

@ -45,46 +45,67 @@ QuantumLearning/
## Quick Start
Use the repo-local bootstrap script on macOS:
Use the consumer-facing control script on macOS:
```bash
cd /Users/oho/GitClone/CodexProjects/QuantumLearning
bash ./scripts/bootstrap_mac.sh
bash ./scripts/app.sh bootstrap
```
That script creates `.venv/`, installs the Python dependencies, and installs the project-local Playwright Chromium runtime into `.playwright-browsers/`.
That creates `.venv/`, installs the Python dependencies, and installs the project-local Playwright Chromium runtime into `.playwright-browsers/`.
For normal consumer use, these are the only commands you should need:
```bash
bash ./scripts/app.sh start --open
bash ./scripts/app.sh status
bash ./scripts/app.sh restart
bash ./scripts/app.sh stop
```
If you want a fast operational check immediately after bootstrap:
```bash
bash ./scripts/project_status.sh
bash ./scripts/run_validation.sh --quick
bash ./scripts/app.sh status
bash ./scripts/app.sh validate --quick
```
Launch Jupyter from the project root with the repo-local wrapper so the notebooks, kernel, and local config stay aligned.
The app command starts Jupyter as a detached background service, prints the exact URL, cleans up stale runtime files when needed, and writes logs to `.logs/jupyterlab.log`.
```bash
./scripts/start_jupyter.sh
bash ./scripts/app.sh start
```
The launcher writes only inside the project directory, opens the repo-local `QuantumLearning (.venv)` kernel, and defaults to the onboarding notebook.
If the server is already running, the same command returns the current URL instead of starting a duplicate server.
If you want a different local port:
```bash
./scripts/start_jupyter.sh 8890
bash ./scripts/app.sh start --port 8890
```
If you want to restart the whole Jupyter layer cleanly:
```bash
bash ./scripts/app.sh restart
```
If you want to inspect the live log:
```bash
bash ./scripts/app.sh logs -f
```
Run the tests:
```bash
bash ./scripts/run_validation.sh --standard
bash ./scripts/app.sh validate --standard
```
Run the measured local coverage report:
```bash
bash ./scripts/run_validation.sh --full
bash ./scripts/app.sh validate --full
```
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.
@ -142,6 +163,14 @@ The older single-notebook sequence is still present as legacy reference material
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.
The lower-level scripts still exist:
- `scripts/bootstrap_mac.sh`
- `scripts/start_jupyter.sh`
- `scripts/project_status.sh`
- `scripts/run_validation.sh`
Those are now plumbing. `scripts/app.sh` is the consumer entrypoint.
## Local-First Design Rules
- Main workflow uses local simulators only.

479
scripts/app.sh Executable file
View file

@ -0,0 +1,479 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="$ROOT_DIR/.logs"
LOG_FILE="$LOG_DIR/jupyterlab.log"
usage() {
cat <<'EOF'
Usage: bash ./scripts/app.sh <command> [options]
Consumer-facing control surface for QuantumLearning.
Commands:
bootstrap [--skip-browser] [--run-validation]
Create or refresh the repo-local environment.
start [--port PORT] [--open] [--foreground] [--restart]
Start the repo-local Jupyter server.
Default mode is background service mode.
stop
Stop the repo-local Jupyter server for this project.
restart [--port PORT] [--open] [--foreground]
Restart the repo-local Jupyter server.
status
Print the current project and Jupyter status.
url
Print the current Jupyter URL for this project.
open
Open the current Jupyter URL in the default browser.
logs [-f|--follow]
Show the Jupyter log file. Use -f to follow live output.
validate [--quick|--standard|--full]
Run the validation suite.
Examples:
bash ./scripts/app.sh bootstrap
bash ./scripts/app.sh start --open
bash ./scripts/app.sh restart
bash ./scripts/app.sh status
bash ./scripts/app.sh stop
bash ./scripts/app.sh validate --quick
EOF
}
die() {
echo "$*" >&2
exit 1
}
ensure_dirs() {
mkdir -p \
"$ROOT_DIR/.cache/matplotlib" \
"$ROOT_DIR/.ipython" \
"$ROOT_DIR/.jupyter_config" \
"$ROOT_DIR/.jupyter_data" \
"$ROOT_DIR/.jupyter_runtime" \
"$LOG_DIR"
}
ensure_venv() {
if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
die "Missing repo-local .venv. Run: bash ./scripts/app.sh bootstrap"
fi
}
set_jupyter_env() {
export PATH="$ROOT_DIR/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export JUPYTER_CONFIG_DIR="$ROOT_DIR/.jupyter_config"
export JUPYTER_DATA_DIR="$ROOT_DIR/.jupyter_data"
export JUPYTER_RUNTIME_DIR="$ROOT_DIR/.jupyter_runtime"
export IPYTHONDIR="$ROOT_DIR/.ipython"
export MPLCONFIGDIR="$ROOT_DIR/.cache/matplotlib"
}
ensure_kernel() {
local kernel_dir="$ROOT_DIR/.venv/share/jupyter/kernels/quantum-learning"
if [[ ! -f "$kernel_dir/kernel.json" ]]; then
"$ROOT_DIR/.venv/bin/python" -m ipykernel install \
--sys-prefix \
--name quantum-learning \
--display-name "QuantumLearning (.venv)" \
--env IPYTHONDIR "$IPYTHONDIR" \
--env MPLCONFIGDIR "$MPLCONFIGDIR"
fi
}
server_json_for_root() {
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR"
from __future__ import annotations
import json
import sys
from pathlib import Path
root = Path(sys.argv[1]).resolve()
runtime_dir = root / ".jupyter_runtime"
matches = []
for path in runtime_dir.glob("jpserver-*.json"):
try:
payload = json.loads(path.read_text())
except Exception:
continue
if Path(payload.get("root_dir", "")).resolve() == root:
matches.append((path.stat().st_mtime, path))
if matches:
print(max(matches)[1])
PY
}
server_json_for_pid() {
local pid="$1"
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "$pid"
from __future__ import annotations
import json
import sys
from pathlib import Path
root = Path(sys.argv[1]).resolve()
target_pid = int(sys.argv[2])
runtime_dir = root / ".jupyter_runtime"
matches = []
for path in runtime_dir.glob("jpserver-*.json"):
try:
payload = json.loads(path.read_text())
except Exception:
continue
if Path(payload.get("root_dir", "")).resolve() == root and int(payload.get("pid", -1)) == target_pid:
matches.append((path.stat().st_mtime, path))
if matches:
print(max(matches)[1])
PY
}
server_field() {
local json_path="$1"
local field_name="$2"
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$json_path" "$field_name"
from __future__ import annotations
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
field_name = sys.argv[2]
payload = json.loads(path.read_text())
value = payload[field_name]
print(value)
PY
}
server_url_from_json() {
local json_path="$1"
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$json_path"
from __future__ import annotations
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
payload = json.loads(path.read_text())
print(f"{payload['url']}lab/tree/notebooks/COURSE_BLUEPRINT.ipynb?token={payload['token']}")
PY
}
cleanup_stale_runtime_files() {
"$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}"
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
root = Path(sys.argv[1]).resolve()
verbose = sys.argv[2] == "1"
runtime_dir = root / ".jupyter_runtime"
removed = []
for path in sorted(runtime_dir.glob("jpserver-*.json")):
try:
payload = json.loads(path.read_text())
except Exception:
continue
if Path(payload.get("root_dir", "")).resolve() != root:
continue
pid = int(payload.get("pid", -1))
alive = False
if pid > 0:
try:
os.kill(pid, 0)
except ProcessLookupError:
alive = False
except PermissionError:
alive = True
else:
alive = True
if alive:
continue
for candidate in (path, path.with_name(f"{path.stem}-open.html")):
if candidate.exists():
candidate.unlink()
removed.append(candidate)
if verbose:
for path in removed:
print(path)
PY
}
have_running_server() {
local json_path
cleanup_stale_runtime_files >/dev/null
json_path="$(server_json_for_root || true)"
if [[ -z "$json_path" ]]; then
return 1
fi
local pid
pid="$(server_field "$json_path" "pid")"
kill -0 "$pid" >/dev/null 2>&1
}
print_running_server() {
local json_path url pid
cleanup_stale_runtime_files >/dev/null
json_path="$(server_json_for_root || true)"
if [[ -z "$json_path" ]]; then
die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
fi
url="$(server_url_from_json "$json_path")"
pid="$(server_field "$json_path" "pid")"
echo "Jupyter URL: $url"
echo "PID: $pid"
echo "Runtime file: $json_path"
echo "Log file: $LOG_FILE"
}
stop_server() {
ensure_venv
ensure_dirs
local json_path pid
cleanup_stale_runtime_files >/dev/null
json_path="$(server_json_for_root || true)"
if [[ -z "$json_path" ]]; then
echo "No repo-local Jupyter runtime file found."
return 0
fi
pid="$(server_field "$json_path" "pid")"
if ! kill -0 "$pid" >/dev/null 2>&1; then
local removed_paths
removed_paths="$(cleanup_stale_runtime_files 1 || true)"
echo "Jupyter runtime file exists, but the process is not running."
echo "Runtime file: $json_path"
if [[ -n "$removed_paths" ]]; then
echo "Removed stale runtime files:"
echo "$removed_paths"
fi
return 0
fi
kill -TERM "$pid"
for _ in $(seq 1 30); do
if ! kill -0 "$pid" >/dev/null 2>&1; then
cleanup_stale_runtime_files >/dev/null
echo "Stopped Jupyter (pid $pid)."
return 0
fi
sleep 1
done
die "Jupyter did not stop cleanly after 30 seconds. Inspect: $LOG_FILE"
}
start_server() {
local port="$1"
local foreground="$2"
local open_browser="$3"
local restart="$4"
ensure_venv
ensure_dirs
set_jupyter_env
ensure_kernel
cleanup_stale_runtime_files >/dev/null
if have_running_server; then
if [[ "$restart" == "1" ]]; then
stop_server
else
echo "Jupyter is already running for this project."
print_running_server
if [[ "$open_browser" == "1" ]]; then
local existing_url
existing_url="$(server_url_from_json "$(server_json_for_root)")"
open "$existing_url" >/dev/null 2>&1 || true
fi
return 0
fi
fi
if [[ "$foreground" == "1" ]]; then
exec "$ROOT_DIR/.venv/bin/jupyter" lab \
--no-browser \
--ip=127.0.0.1 \
--port="$port"
fi
: > "$LOG_FILE"
nohup "$ROOT_DIR/.venv/bin/jupyter" lab \
--no-browser \
--ip=127.0.0.1 \
--port="$port" \
< /dev/null \
>>"$LOG_FILE" 2>&1 &
local launch_pid=$!
local json_path=""
for _ in $(seq 1 60); do
if ! kill -0 "$launch_pid" >/dev/null 2>&1; then
echo "Jupyter exited during startup. Recent log output:" >&2
tail -n 40 "$LOG_FILE" >&2 || true
exit 1
fi
json_path="$(server_json_for_pid "$launch_pid" || true)"
if [[ -n "$json_path" ]]; then
local url
url="$(server_url_from_json "$json_path")"
echo "Jupyter started."
echo "URL: $url"
echo "PID: $launch_pid"
echo "Log: $LOG_FILE"
if [[ "$open_browser" == "1" ]]; then
open "$url" >/dev/null 2>&1 || true
fi
return 0
fi
sleep 1
done
die "Timed out waiting for Jupyter startup. Inspect: $LOG_FILE"
}
show_logs() {
ensure_dirs
if [[ ! -f "$LOG_FILE" ]]; then
echo "No Jupyter log file exists yet."
return 0
fi
if [[ "${1:-}" == "--follow" || "${1:-}" == "-f" ]]; then
tail -f "$LOG_FILE"
else
tail -n 80 "$LOG_FILE"
fi
}
validate_command() {
bash "$ROOT_DIR/scripts/run_validation.sh" "$@"
}
start_command() {
local port="8888"
local foreground="0"
local open_browser="0"
local restart="0"
while [[ $# -gt 0 ]]; do
case "$1" in
--port)
[[ $# -ge 2 ]] || die "--port requires a value"
port="$2"
shift 2
;;
--open)
open_browser="1"
shift
;;
--foreground)
foreground="1"
shift
;;
--restart)
restart="1"
shift
;;
-h|--help)
cat <<'EOF'
Usage: bash ./scripts/app.sh start [--port PORT] [--open] [--foreground] [--restart]
EOF
return 0
;;
*)
die "Unknown start option: $1"
;;
esac
done
[[ "$port" =~ ^[0-9]+$ ]] || die "Port must be an integer, got: $port"
start_server "$port" "$foreground" "$open_browser" "$restart"
}
restart_command() {
start_command --restart "$@"
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
COMMAND="$1"
shift
case "$COMMAND" in
bootstrap)
exec bash "$ROOT_DIR/scripts/bootstrap_mac.sh" "$@"
;;
start)
start_command "$@"
;;
stop)
stop_server
;;
restart)
restart_command "$@"
;;
status)
exec bash "$ROOT_DIR/scripts/project_status.sh"
;;
url)
ensure_venv
cleanup_stale_runtime_files >/dev/null
json_path="$(server_json_for_root || true)"
[[ -n "$json_path" ]] || die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
server_url_from_json "$json_path"
;;
open)
ensure_venv
cleanup_stale_runtime_files >/dev/null
json_path="$(server_json_for_root || true)"
[[ -n "$json_path" ]] || die "No repo-local Jupyter runtime file found. Run: bash ./scripts/app.sh start"
url="$(server_url_from_json "$json_path")"
open "$url"
echo "Opened: $url"
;;
logs)
show_logs "${1:-}"
;;
validate)
validate_command "$@"
;;
-h|--help|help)
usage
;;
*)
usage >&2
die "Unknown command: $COMMAND"
;;
esac

View file

@ -90,7 +90,7 @@ cat <<EOF
Bootstrap complete.
Next steps:
bash ./scripts/project_status.sh
bash ./scripts/run_validation.sh --quick
./scripts/start_jupyter.sh
bash ./scripts/app.sh status
bash ./scripts/app.sh validate --quick
bash ./scripts/app.sh start --open
EOF

View file

@ -44,6 +44,12 @@ else
echo "Browser runtime: missing"
fi
if [[ -f "$ROOT_DIR/.logs/jupyterlab.log" ]]; then
echo "Jupyter log: $ROOT_DIR/.logs/jupyterlab.log"
else
echo "Jupyter log: none yet"
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"
@ -76,6 +82,11 @@ else
echo "Jupyter: no repo-local runtime file found"
fi
echo "Consumer:"
echo " Start: bash ./scripts/app.sh start --open"
echo " Stop: bash ./scripts/app.sh stop"
echo " Restart: bash ./scripts/app.sh restart"
echo " Logs: bash ./scripts/app.sh logs -f"
echo "Validation:"
echo " Quick: bash ./scripts/run_validation.sh --quick"
echo " Full: bash ./scripts/run_validation.sh --full"
echo " Quick: bash ./scripts/app.sh validate --quick"
echo " Full: bash ./scripts/app.sh validate --full"

View file

@ -2,55 +2,9 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PORT="${1:-8888}"
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
cat <<'EOF'
Usage: ./scripts/start_jupyter.sh [port]
Starts repo-local JupyterLab from .venv with project-local config, runtime,
kernel registration, and notebook defaults.
EOF
exit 0
if [[ $# -gt 0 && "$1" =~ ^[0-9]+$ ]]; then
exec bash "$ROOT_DIR/scripts/app.sh" start --port "$1"
fi
if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
echo "Port must be an integer, got: $PORT" >&2
exit 1
fi
if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
echo "Missing repo-local .venv at $ROOT_DIR/.venv" >&2
echo "Create it first with: python3.12 -m venv .venv" >&2
exit 1
fi
mkdir -p \
"$ROOT_DIR/.cache/matplotlib" \
"$ROOT_DIR/.ipython" \
"$ROOT_DIR/.jupyter_config" \
"$ROOT_DIR/.jupyter_data" \
"$ROOT_DIR/.jupyter_runtime"
# Keep JupyterLab away from the broken Homebrew node installation on this machine.
export PATH="$ROOT_DIR/.venv/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export JUPYTER_CONFIG_DIR="$ROOT_DIR/.jupyter_config"
export JUPYTER_DATA_DIR="$ROOT_DIR/.jupyter_data"
export JUPYTER_RUNTIME_DIR="$ROOT_DIR/.jupyter_runtime"
export IPYTHONDIR="$ROOT_DIR/.ipython"
export MPLCONFIGDIR="$ROOT_DIR/.cache/matplotlib"
KERNEL_DIR="$ROOT_DIR/.venv/share/jupyter/kernels/quantum-learning"
if [[ ! -f "$KERNEL_DIR/kernel.json" ]]; then
"$ROOT_DIR/.venv/bin/python" -m ipykernel install \
--sys-prefix \
--name quantum-learning \
--display-name "QuantumLearning (.venv)" \
--env IPYTHONDIR "$IPYTHONDIR" \
--env MPLCONFIGDIR "$MPLCONFIGDIR"
fi
exec "$ROOT_DIR/.venv/bin/jupyter" lab \
--no-browser \
--ip=127.0.0.1 \
--port="$PORT"
exec bash "$ROOT_DIR/scripts/app.sh" start "$@"

View file

@ -18,6 +18,19 @@ def test_bootstrap_script_help_works():
assert "Usage:" in result.stdout
def test_app_script_help_works():
result = subprocess.run(
["bash", "scripts/app.sh", "--help"],
cwd=project_root(),
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0
assert "Consumer-facing control surface" in result.stdout
def test_validation_script_help_works():
result = subprocess.run(
["bash", "scripts/run_validation.sh", "--help"],
@ -45,12 +58,32 @@ def test_project_status_script_runs():
assert "Validation:" in result.stdout
def test_app_status_runs():
result = subprocess.run(
["bash", "scripts/app.sh", "status"],
cwd=project_root(),
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0
assert "Consumer:" in result.stdout
def test_app_script_uses_detached_launch_and_stale_runtime_cleanup():
text = (project_root() / "scripts" / "app.sh").read_text()
assert "cleanup_stale_runtime_files" in text
assert "nohup " in text
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",
"bash ./scripts/app.sh bootstrap",
"bash ./scripts/app.sh status",
"bash ./scripts/app.sh validate --quick",
]
for relative_path in ("README.md", "FIRST_RUN.md", "OPERATIONS.md"):

View file

@ -83,6 +83,10 @@ def test_operations_guide_exists():
assert (project_root() / "OPERATIONS.md").exists()
def test_consumer_app_script_exists():
assert (project_root() / "scripts" / "app.sh").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()