refactor(server): carve demo mode into lib/demo_mode.py (R3b) (#903)

lib/demo_mode.py (342). server.py 1,870 -> 1,649.

The read-only request guard (its 96-entry blocked-route table + the middleware) and the
hourly session janitor (registry, hook runner, thread). Bodies VERBATIM.

THE MIDDLEWARE NEEDS `app`, SO THE MODULE TAKES IT. _demo_mode_guard is an
@app.middleware("http") and cannot exist without an app object. Rather than have a module
under lib/ reach for a global, it exposes install(app) and server.py — which owns the app —
hands it over. The janitor is symmetrical: start_janitor() / stop_janitor(), called from
server.py's startup and shutdown hooks, where the process lifecycle actually lives.

register_demo_janitor_hook IS PART OF THE PLUGIN CONTRACT. It is a key in plugin_context,
so plugins hold it as a LIVE REFERENCE from setup(). server.py imports this exact object
and puts it in the dict unchanged — identity preserved, and
tests/test_plugin_context_contract.py (#898, merged) fails if that ever stops being true.
This is the first carve that guard has actually protected.

━━━ stop_janitor()'s ORDER IS LOAD-BEARING ━━━

The obvious way to write it — clear the "started" flag, then join — is WRONG, and I wrote
it that way first. server.py's original deliberately returns EARLY, leaving
_DEMO_JANITOR_STARTED True and the thread handle intact, when the thread outlives the join:

    # Leave _DEMO_JANITOR_STARTED True so a new janitor is not
    # spawned by a subsequent startup while the old one is alive.

Clearing the flag first quietly reintroduces exactly the double-janitor leak the flag
exists to prevent. Preserved byte-for-byte, and the reason is now written down at the
function rather than only at its single call site.

━━━ A BUG MOVED VERBATIM, ON PURPOSE ━━━

    if getenv_compat("FEEDBACK_DEMO_MODE") or getenv_compat("FEEDBACK_DEMO_MODE") == "1" \
            and not _DEMO_JANITOR_STARTED:

`and` binds tighter than `or`, so this is `A or (B and C)` — the not-already-started
re-entry guard is DEAD whenever the env var is truthy, which is the only case that runs.
A second startup leaks a janitor thread (the handle is overwritten, so shutdown joins only
the last). Verified. Preserved exactly and filed as issue #902: a carve whose whole value
is being provably behaviour-neutral is not the place to change behaviour.

pyflakes caught three more missing imports on the way in (uuid, warnings x2). Five carves,
ten missing imports, every one a NameError on a live path.

pytest 2399, pyflakes 0, Codex 0.

Refs #48

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-12 01:32:46 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 8f014e6a30
commit f5d448af5c
4 changed files with 404 additions and 281 deletions
+17 -16
View File
@@ -11,6 +11,7 @@ import time
import asyncio
import httpx
import demo_mode
import pytest
from fastapi.testclient import TestClient
@@ -120,12 +121,12 @@ def startup_harness(tmp_path, monkeypatch, isolate_logging):
yield server, phases
server._DEMO_JANITOR_STOP.set()
thread = server._DEMO_JANITOR_THREAD
demo_mode._DEMO_JANITOR_STOP.set()
thread = demo_mode._DEMO_JANITOR_THREAD
if thread is not None:
thread.join(timeout=2)
server._DEMO_JANITOR_STARTED = False
server._DEMO_JANITOR_THREAD = None
demo_mode._DEMO_JANITOR_STARTED = False
demo_mode._DEMO_JANITOR_THREAD = None
conn = getattr(getattr(server, "meta_db", None), "conn", None)
if conn is not None:
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
@@ -686,12 +687,12 @@ def test_startup_status_e2e_real_plugin_loader(tmp_path, monkeypatch, isolate_lo
assert sentinel.status_code == 200
assert sentinel.json() == {"ok": True}
finally:
server._DEMO_JANITOR_STOP.set()
thread = server._DEMO_JANITOR_THREAD
demo_mode._DEMO_JANITOR_STOP.set()
thread = demo_mode._DEMO_JANITOR_THREAD
if thread is not None:
thread.join(timeout=2)
server._DEMO_JANITOR_STARTED = False
server._DEMO_JANITOR_THREAD = None
demo_mode._DEMO_JANITOR_STARTED = False
demo_mode._DEMO_JANITOR_THREAD = None
conn = getattr(getattr(server, "meta_db", None), "conn", None)
if conn is not None:
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
@@ -777,12 +778,12 @@ def test_startup_status_endpoint_background_thread_path(tmp_path, monkeypatch, i
# actually executed the sentinel — proves the main-loop handoff path ran.
assert _route_setup_called, "route_setup_fn was never called; call_soon_threadsafe path was not exercised"
finally:
server._DEMO_JANITOR_STOP.set()
thread = server._DEMO_JANITOR_THREAD
demo_mode._DEMO_JANITOR_STOP.set()
thread = demo_mode._DEMO_JANITOR_THREAD
if thread is not None:
thread.join(timeout=2)
server._DEMO_JANITOR_STARTED = False
server._DEMO_JANITOR_THREAD = None
demo_mode._DEMO_JANITOR_STARTED = False
demo_mode._DEMO_JANITOR_THREAD = None
conn = getattr(getattr(server, "meta_db", None), "conn", None)
if conn is not None:
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
@@ -826,12 +827,12 @@ def test_startup_status_endpoint_background_thread_failure(tmp_path, monkeypatch
assert data["phase"] == "error"
assert _BG_ERROR in data["error"]
finally:
server._DEMO_JANITOR_STOP.set()
thread = server._DEMO_JANITOR_THREAD
demo_mode._DEMO_JANITOR_STOP.set()
thread = demo_mode._DEMO_JANITOR_THREAD
if thread is not None:
thread.join(timeout=2)
server._DEMO_JANITOR_STARTED = False
server._DEMO_JANITOR_THREAD = None
demo_mode._DEMO_JANITOR_STARTED = False
demo_mode._DEMO_JANITOR_THREAD = None
conn = getattr(getattr(server, "meta_db", None), "conn", None)
if conn is not None:
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()