refactor(server): carve demo mode into lib/demo_mode.py (R3b) - #903
Merged
Conversation
Contributor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughDemo mode is extracted into ChangesDemo mode and scan lifecycle integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant FastAPIApp
participant demo_mode_guard
participant Endpoint
Client->>FastAPIApp: Send HTTP request
FastAPIApp->>demo_mode_guard: Run HTTP middleware
demo_mode_guard->>Endpoint: Forward allowed request
Endpoint-->>Client: Return response
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
byrongamatos
force-pushed
the
r3b/demo-mode
branch
from
July 11, 2026 23:23
9bedfb2 to
5da720b
Compare
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>
byrongamatos
force-pushed
the
r3b/demo-mode
branch
from
July 11, 2026 23:28
5da720b to
22c24eb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_guardis an@app.middleware("http")and cannot exist without an app object. Rather than have a module underlib/reach for a global, it exposesinstall(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_hookis part of the plugin contractIt's a key in
plugin_context, so plugins hold it as a live reference fromsetup(). server.py imports this exact object and puts it in the dict unchanged — identity preserved, andtests/test_plugin_context_contract.py(#898) fails if that ever stops being true.This is the first carve that guard has actually protected.
stop_janitor()'s order is load-bearingThe 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_STARTEDTrue and the thread handle intact, when the thread outlives the join: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
andbinds tighter thanor, so this isA 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 #902. A carve whose whole value is being provably behaviour-neutral is not the place to change behaviour.
pyflakes
Three more missing imports caught on the way in (
uuid,warnings). Five carves, ten missing imports, every one aNameErroron a live path.pytest 2399 · pyflakes 0 · Codex 0.
🤖 Generated with Claude Code
Summary by CodeRabbit