From 377c96de8a6d3961b867c8f629b73ff14639e3e2 Mon Sep 17 00:00:00 2001 From: saymrwulf Date: Wed, 15 Apr 2026 15:29:01 +0200 Subject: [PATCH] Harden Jupyter state lifecycle and cleanup --- .gitignore | 2 + .jupyter_data/nbsignatures.db | Bin 16384 -> 0 bytes .jupyter_data/notebook_secret | 18 --- FIRST_RUN.md | 7 ++ OPERATIONS.md | 9 ++ README.md | 7 ++ scripts/app.sh | 231 +++++++++++++++++++++++++++++++++- scripts/project_status.sh | 77 +----------- tests/test_operations.py | 12 ++ 9 files changed, 268 insertions(+), 95 deletions(-) delete mode 100644 .jupyter_data/nbsignatures.db delete mode 100644 .jupyter_data/notebook_secret mode change 100755 => 100644 scripts/project_status.sh diff --git a/.gitignore b/.gitignore index c5f0284..e4f56f0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .ipython/ .jupyter_runtime/ .jupyter_data/lab/workspaces/ +.jupyter_data/nbsignatures.db +.jupyter_data/notebook_secret __pycache__/ .pytest_cache/ .ruff_cache/ diff --git a/.jupyter_data/nbsignatures.db b/.jupyter_data/nbsignatures.db deleted file mode 100644 index d2e8ace25955000435b7353756b30f3277e045b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHNOOG5^74G)f^L{ulKPHf)yaKVao~uE4HiAOK(vY2)<6bt*Y?^7KMa}s-}Bps_WMI>U`&%@7$Ko z+h4hTFfF6{&f~){9r57&U^pCnVKf>H2Cv|K7VpJz2LCkAhl>mU=I<+m@ee=R!h)r3 z9&A6^{u%!CN6$ddK+izWK+izWK+izWK+izWK+izW!2bsW50+LgzI+b7&UmMLAjNZIEYA(7u`~302^x@%+(bHS6 zt&bYVTW6LpUcWxPKMnCwIezaEzNmiVxV-m%IZ7u_m*$ZBH@|p$Z**eq)uV7&Zs5*q z-`g3kT)c8+`2Fy)&F1qo|3A~R*1XbszN@XX*@GYC(ZSKQJSdMxZ{53f=jOfpqc`^M zk8XZ-|IJ%>Vf4=4-TfQS-1U@+(X@OpefIiOdOqd)yJ7n9Q!YIU$5U`tjz-gi!*VtA1cvi6;|^Q%8! zl`Fqn*-6qFC4*?DM47W6ghk2lvFZVw%TjylMAviDVE76n{qCKVxrUFq|~0(9PUyJ zZUz-h&AR3sIF(Xj5Xzb?IELzs%tF2Rw6_*-n5?UA7sHKEZK$cR{C2gvWo;l4httNb=WR@m^@RMW$PHV;N=5QCp?W|UXGBR9Yaoq?W^Fku3Q=n+1Xn4jnn9#9*16N*ls4WkV)s%D&fDsZFIAA1 zMSF5q31W??IHZh~LPbqgM6SpxA3(eW@iKd>6+$gfgJVx#)7-lkTW~TdaN+{106Q$C ztr2;UdQ;Dn39lBBGUMI*AHo zV`~i@Ga!j-RNf<XX#BrG(Z?X%ti&M8RUB6eFXxabn}gKDk1I)VjlYcM7oR97^@ zgHT&!K`1$?fHgrwhC(4S$xzCtd1sCBW}%szEx2M!3J8>Bra;L25@rt#DPx2Yg5rQD zGP*(%41Eg;NtzP^3~klfoCb%dL}!_kxzU0P5~_g;K-`%n97;jF%^*XdJmhl(o0{8j!lTm1)>>%AJa#K>xQJScDBPs9%*f!(5m};q%*B$* zk{w!8(CFaN$0RE$td+W^5KulRydyc$Zmhjw$#b~n796Be)HKVMQW(T}k;$W5dz86G z9hMYT)=@Ib+CtzmqiRZW)goJRYH6mO08TmVRUnOv%skhEgBzlvmZBCAMQ2m2Ng`x6 z`&?Z$In6Q&3JzLj$uf{bjb;|uDrsnYn@q_QvLMsIi@A4aTW}G3Dneowv@flYDG`rP zvz9AnHJGp&!_GmVl)UxMn1Gtl*k?c$(HyboY88S6nQCm%p?J}NmRfMY&>yQusG5)u zR%xeD<|tx^SXNn}+@dNrNKeKZBrwe+kqjEII#~!}LS~G_grT{2XIgL`u2u5b6htx8 z353uqTi~IL8QV}au8fVKHVL~Sr6B?w9D7J<@G8&=i6${&JBHut>DFl2g2TRmrGO;i z{;YN28eI@fPB>ps`fy5;O~LzUu}3V})H#rh^|Q^)Nf84aVyB&PH1}@y{eQUgWPn$H z^bGV2^bGV2^bGV2^bGV2^bGV2^bGV2^bGV2d?pw;KU}&pY|b<6uEVe0)z=1FKi&G< z>bJITufDeSj}5W?-EH3d=lWk(f4}kVod;{b-2Bz{pEfV;{AS}1>pxygYZunPytBJm zJ`+Hu)(^%KZ zj_szgu5%k(O=DfBGB%sWy3Sl|G>vtgs90|r>pCy7)-=|28e+9&tmCZ1O4C@^NrvU7 nv95Cp=bFa4P9dCa8tXb^u+%ixb%Nl`8?z?7_DK`&Iv?;al*^Dw diff --git a/.jupyter_data/notebook_secret b/.jupyter_data/notebook_secret deleted file mode 100644 index ef26861..0000000 --- a/.jupyter_data/notebook_secret +++ /dev/null @@ -1,18 +0,0 @@ -SrXNuCCg+wrEA1L1jVumnXf3ChsNATdtqH3UiZud+nbMUHxjUqqRRPcHmyufdB8eMh0TPu+cIe8Z -t0caRkarRaz/ouQ9rwyFjxgt4UMFFRHXqAofkPH4zF89S4s83U+C2mZ911tUoAr7rql6lW0UDrro -U4RYWQFd356AfiDwmGhC2AVO7/Yt2NmOgawxWvqsuAaIzSIjDvFY/kU+UNu2nyW7FrhumjgmBp93 -Hk4BOZb8xnEcz24E9wVQ/9Fn2XXcIFGB+DowTQPkai3xIo2zx1o5+6+Vbsq1b7dfESlliXj6Nh0o -R3haoHFr4B6Q/biz7YMZq3njh2Mq/6NzoK71exnJ4vjm6/ucLlX94DEeyvlsK8XtBf+rO/Ux2kQ4 -P8U3VqqEcsFE36k7jVGqMv00m/PKL1W4A/C2HdutD6WsGDu8F6+jI3lKgrWVcylwqZf0t0TQ9zKf -pqlFO1PmsIubDRwd6rfiZbBC8S1fMzBMWfs4jM2FVWALIz+fQZZ2pUBxmMPsk3M7SmmKw8Zyc/nj -U+SroBGM1eb4W7UERCCsa990B00zmNCz5s/VJkrn9TVwAT48vVuxphKPxMeyiwtHuEHiSkN2fb/l -8aaaL1DiFk8KSogKsJZ9JgEpnqMXqxEJ6JF+IRJrfHHD8jPuatXjPqVjdMgpSnWG6ngrbAd2cw4A -lHOqJG8EuRieYh/vx/xc+b8+LumXol0m1Gz7W1pVRXXrh+rVrMk3eUJO4yAbcXIZM3/Sqe8N7ZAH -yxtzFCce5mgNGdk+1I3GfLTU999fbdERodpsnlKoSX7eB+fT+8z68kucSx4OYswIaXLCk58Fjz+1 -ZuvO8SsiNoSoQ9tIv1B0aKFGST4BLyQ1NccTIJjhAGiE65jzpDuaqRl9HyTXF3Wj7TeSlOYbOJLh -1H3CupTXl3PjH9i6K1zrx4TrOEe5J4b/iaIXlw8PzUSBL/yeaGHou2NzW2oTOQ1Ev9P9n9SfyIU3 -BuCvqiUTbm2k8Ktk9g7mcHUw+63syE0b9CTPXzdAlvI30j+K2s+zs6fWtpZXsIy+Xz9rcBX3Nztm -nl/Z3yLa3+khqS7hY/kxgURjvm0XxPwuGoWt9dJFkkoQXLiOrzKiCD773qwrLHEyHx0TI4ntfFV0 -TducvMBpiAvaN1KnlJU8K6yyeqZdJytxJngZGcRgtz/IjySByb75zLpInoHf0HKxgjPfSHvXCVlP -vuKq5a16PU7Dlj01u3szIR16bh/qXeKn3h89gslw0fR+ILWnzTuy8VlqtbgfSW+Ohj5X5VCok6k/ -dr21sWY2J1GlZmu9N/4Hpj49kORRj/lb5UXZIGsqxSeK4yHDGTGR1YBK+GPmARkzzvF/qDY1FQ== diff --git a/FIRST_RUN.md b/FIRST_RUN.md index 2f5afa1..87b35ae 100644 --- a/FIRST_RUN.md +++ b/FIRST_RUN.md @@ -23,6 +23,13 @@ Use this the first time you open the platform, or anytime you want a clean local bash ./scripts/app.sh start --open ``` + If JupyterLab later looks haunted by old tabs or stale session state, reset the repo-local Jupyter state and start again: + + ```bash + bash ./scripts/app.sh reset-state + bash ./scripts/app.sh start --open + ``` + 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`. diff --git a/OPERATIONS.md b/OPERATIONS.md index 970ac06..def7ae4 100644 --- a/OPERATIONS.md +++ b/OPERATIONS.md @@ -77,6 +77,7 @@ Other normal consumer commands: bash ./scripts/app.sh status bash ./scripts/app.sh restart bash ./scripts/app.sh stop +bash ./scripts/app.sh reset-state bash ./scripts/app.sh logs -f ``` @@ -106,11 +107,18 @@ It reports: - 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 +- whether saved Jupyter UI state is still present and how to reset it 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. +If JupyterLab reopens old tabs or appears to remember stale notebook activity, that is usually saved workspace state rather than a still-running repo-local server. Clear it with: + +```bash +bash ./scripts/app.sh reset-state +``` + ## Validation Modes Quick local validation: @@ -176,6 +184,7 @@ If something feels wrong, use this order: 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` +5. if the UI still looks stale, run `bash ./scripts/app.sh reset-state` If `.venv/` is missing or broken, re-run: diff --git a/README.md b/README.md index cbd5d72..e2a80d4 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ bash ./scripts/app.sh start --open bash ./scripts/app.sh status bash ./scripts/app.sh restart bash ./scripts/app.sh stop +bash ./scripts/app.sh reset-state ``` If you want a fast operational check immediately after bootstrap: @@ -96,6 +97,12 @@ If you want to inspect the live log: bash ./scripts/app.sh logs -f ``` +If JupyterLab reopens old tabs, looks like notebooks are still running, or you want a true clean slate for the repo-local Jupyter state: + +```bash +bash ./scripts/app.sh reset-state +``` + Run the tests: ```bash diff --git a/scripts/app.sh b/scripts/app.sh index 57803de..d036963 100755 --- a/scripts/app.sh +++ b/scripts/app.sh @@ -22,6 +22,9 @@ Commands: stop Stop the repo-local Jupyter server for this project. + reset-state + Remove repo-local Jupyter runtime and saved UI state files. + restart [--port PORT] [--open] [--foreground] Restart the repo-local Jupyter server. @@ -46,6 +49,7 @@ Examples: bash ./scripts/app.sh restart bash ./scripts/app.sh status bash ./scripts/app.sh stop + bash ./scripts/app.sh reset-state bash ./scripts/app.sh validate --quick EOF } @@ -224,6 +228,90 @@ if verbose: PY } +purge_runtime_artifacts() { + "$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}" +from __future__ import annotations + +import sys +from pathlib import Path + +root = Path(sys.argv[1]).resolve() +verbose = sys.argv[2] == "1" +runtime_dir = root / ".jupyter_runtime" +patterns = ( + "jpserver-*.json", + "jpserver-*-open.html", + "nbserver-*.json", + "nbserver-*-open.html", + "kernel-*.json", +) +removed = [] + +for pattern in patterns: + for path in sorted(runtime_dir.glob(pattern)): + if path.exists(): + path.unlink() + removed.append(path) + +if verbose: + for path in removed: + print(path) +PY +} + +purge_saved_ui_state() { + "$ROOT_DIR/.venv/bin/python" - <<'PY' "$ROOT_DIR" "${1:-0}" +from __future__ import annotations + +import sys +from pathlib import Path + +root = Path(sys.argv[1]).resolve() +verbose = sys.argv[2] == "1" +data_dir = root / ".jupyter_data" +paths = [ + data_dir / "nbsignatures.db", + data_dir / "notebook_secret", +] +workspace_dir = data_dir / "lab" / "workspaces" +removed = [] + +for path in paths: + if path.exists(): + path.unlink() + removed.append(path) + +for path in sorted(workspace_dir.glob("*.jupyterlab-workspace")): + if path.exists(): + path.unlink() + removed.append(path) + +if verbose: + for path in removed: + print(path) +PY +} + +print_ui_state_summary() { + local workspace_count="0" + if compgen -G "$ROOT_DIR/.jupyter_data/lab/workspaces/*.jupyterlab-workspace" >/dev/null; then + workspace_count="$(find "$ROOT_DIR/.jupyter_data/lab/workspaces" -maxdepth 1 -name '*.jupyterlab-workspace' | wc -l | tr -d ' ')" + fi + + local trust_db="absent" + local notebook_secret="absent" + [[ -f "$ROOT_DIR/.jupyter_data/nbsignatures.db" ]] && trust_db="present" + [[ -f "$ROOT_DIR/.jupyter_data/notebook_secret" ]] && notebook_secret="present" + + if [[ "$workspace_count" == "0" && "$trust_db" == "absent" && "$notebook_secret" == "absent" ]]; then + echo "Jupyter UI state: clean" + return 0 + fi + + echo "Jupyter UI state: workspaces=$workspace_count, trust-db=$trust_db, notebook-secret=$notebook_secret" + echo " Clean slate: bash ./scripts/app.sh reset-state" +} + have_running_server() { local json_path cleanup_stale_runtime_files >/dev/null @@ -256,24 +344,34 @@ stop_server() { ensure_venv ensure_dirs - local json_path pid + local json_path pid removed_paths 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." + removed_paths="$(purge_runtime_artifacts 1 || true)" + if [[ -n "$removed_paths" ]]; then + echo "Removed stale runtime files:" + echo "$removed_paths" + fi 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)" + local extra_removed + extra_removed="$(purge_runtime_artifacts 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 + if [[ -n "$extra_removed" ]]; then + echo "Removed leftover runtime artifacts:" + echo "$extra_removed" + fi return 0 fi @@ -281,6 +379,7 @@ stop_server() { for _ in $(seq 1 30); do if ! kill -0 "$pid" >/dev/null 2>&1; then cleanup_stale_runtime_files >/dev/null + purge_runtime_artifacts >/dev/null echo "Stopped Jupyter (pid $pid)." return 0 fi @@ -317,6 +416,8 @@ start_server() { fi fi + purge_runtime_artifacts >/dev/null + if [[ "$foreground" == "1" ]]; then exec "$ROOT_DIR/.venv/bin/jupyter" lab \ --no-browser \ @@ -378,6 +479,124 @@ validate_command() { bash "$ROOT_DIR/scripts/run_validation.sh" "$@" } +reset_state_command() { + ensure_venv + ensure_dirs + + if have_running_server; then + stop_server >/dev/null + else + cleanup_stale_runtime_files >/dev/null + purge_runtime_artifacts >/dev/null + fi + + local removed_runtime removed_ui + removed_runtime="$(purge_runtime_artifacts 1 || true)" + removed_ui="$(purge_saved_ui_state 1 || true)" + + if [[ -z "$removed_runtime" && -z "$removed_ui" ]]; then + echo "Jupyter state was already clean." + return 0 + fi + + if [[ -n "$removed_runtime" ]]; then + echo "Removed runtime artifacts:" + echo "$removed_runtime" + fi + + if [[ -n "$removed_ui" ]]; then + echo "Removed saved UI state:" + echo "$removed_ui" + fi +} + +status_command() { + cd "$ROOT_DIR" + ensure_dirs + + local branch commit git_state + 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 + local python_version removed_runtime json_path pid url + python_version="$("$ROOT_DIR/.venv/bin/python" -c 'import sys; print(sys.version.split()[0])')" + echo "Venv: present (Python $python_version)" + cleanup_stale_runtime_files >/dev/null + if ! have_running_server; then + removed_runtime="$(purge_runtime_artifacts 1 || true)" + else + removed_runtime="" + fi + json_path="$(server_json_for_root || true)" + + if compgen -G "$ROOT_DIR/.playwright-browsers/chromium-*" >/dev/null; then + echo "Browser runtime: installed" + else + echo "Browser runtime: missing" + fi + + if [[ -f "$LOG_FILE" ]]; then + echo "Jupyter log: $LOG_FILE" + else + echo "Jupyter log: none yet" + fi + + if [[ -n "$json_path" ]]; then + pid="$(server_field "$json_path" "pid")" + url="$(server_url_from_json "$json_path")" + if kill -0 "$pid" >/dev/null 2>&1; then + echo "Jupyter: running (pid $pid)" + echo " URL: $url" + echo " Runtime file: $json_path" + else + echo "Jupyter: runtime file exists but process is not running" + echo " Runtime file: $json_path" + fi + else + echo "Jupyter: no repo-local runtime file found" + if [[ -n "${removed_runtime:-}" ]]; then + echo " Purged stale runtime artifacts:" + echo "$removed_runtime" + fi + fi + + print_ui_state_summary + else + echo "Venv: missing" + if compgen -G "$ROOT_DIR/.playwright-browsers/chromium-*" >/dev/null; then + echo "Browser runtime: installed" + else + echo "Browser runtime: missing" + fi + if [[ -f "$LOG_FILE" ]]; then + echo "Jupyter log: $LOG_FILE" + else + echo "Jupyter log: none yet" + fi + echo "Jupyter: repo-local environment is missing" + print_ui_state_summary + 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 " Reset: bash ./scripts/app.sh reset-state" + echo " Logs: bash ./scripts/app.sh logs -f" + echo "Validation:" + echo " Quick: bash ./scripts/app.sh validate --quick" + echo " Full: bash ./scripts/app.sh validate --full" +} + start_command() { local port="8888" local foreground="0" @@ -441,11 +660,17 @@ case "$COMMAND" in stop) stop_server ;; + reset-state) + reset_state_command "$@" + ;; restart) restart_command "$@" ;; status) - exec bash "$ROOT_DIR/scripts/project_status.sh" + status_command + ;; + _status_internal) + status_command ;; url) ensure_venv diff --git a/scripts/project_status.sh b/scripts/project_status.sh old mode 100755 new mode 100644 index 0cd0990..aa57f54 --- a/scripts/project_status.sh +++ b/scripts/project_status.sh @@ -8,8 +8,8 @@ usage() { 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. +git state, repo-local Python environment, Playwright browser runtime, +repo-local Jupyter status, and saved Jupyter UI state. EOF } @@ -18,75 +18,4 @@ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then 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 [[ -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" -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 "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/app.sh validate --quick" -echo " Full: bash ./scripts/app.sh validate --full" +exec bash "$ROOT_DIR/scripts/app.sh" _status_internal diff --git a/tests/test_operations.py b/tests/test_operations.py index ca592c6..5501316 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -56,6 +56,7 @@ def test_project_status_script_runs(): assert result.returncode == 0 assert "Git:" in result.stdout assert "Validation:" in result.stdout + assert "Jupyter UI state:" in result.stdout def test_app_status_runs(): @@ -69,12 +70,15 @@ def test_app_status_runs(): assert result.returncode == 0 assert "Consumer:" in result.stdout + assert "Reset:" 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 "purge_runtime_artifacts" in text + assert "reset-state" in text assert "nohup " in text @@ -83,6 +87,7 @@ def test_operations_docs_use_tested_command_forms(): expected_commands = [ "bash ./scripts/app.sh bootstrap", "bash ./scripts/app.sh status", + "bash ./scripts/app.sh reset-state", "bash ./scripts/app.sh validate --quick", ] @@ -90,3 +95,10 @@ def test_operations_docs_use_tested_command_forms(): text = (root / relative_path).read_text() for command in expected_commands: assert command in text + + +def test_jupyter_runtime_noise_is_ignored(): + text = (project_root() / ".gitignore").read_text() + + assert ".jupyter_data/nbsignatures.db" in text + assert ".jupyter_data/notebook_secret" in text