cm_bot_v2/docs/superpowers/plans/2026-05-02-debug-mode-hotfix.md
yiekheng 40c3a76c13 Add implementation plan for debug-mode hotfix
Bite-sized TDD-style plan: failing helper test, two implementation
tasks (web then api), compose plumbing, doc updates, integration
verification. Uses unittest stdlib so no new deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:20:50 +08:00

622 lines
20 KiB
Markdown

# Debug-Mode Hotfix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace hardcoded `debug=True` in both Flask entrypoints with an env-driven `CM_DEBUG` toggle so the Werkzeug debugger is off by default in rex/siong containers and only enabled when an operator opts in.
**Architecture:** Add a small `_debug_enabled()` helper to `app/cm_api.py` and `app/cm_web_view.py` that reads `CM_DEBUG` from the environment and accepts `1`/`true`/`yes` (case-insensitive) as truthy. Wire `CM_DEBUG: ${CM_DEBUG:-false}` into the Flask service `environment:` blocks in `docker-compose.yml`. Document the variable in `.env.example` and `AGENTS.md`. Gate against regressions with a `unittest`-based test that exercises both copies of the helper.
**Tech Stack:** Python 3.9 (containers), Python 3.12 (local venv), Flask 2.3.3, Docker Compose v2, `unittest` (stdlib — no new dependency).
**Spec:** [docs/superpowers/specs/2026-05-02-debug-mode-hotfix-design.md](../specs/2026-05-02-debug-mode-hotfix-design.md)
---
## File Map
| File | Operation | Purpose |
|---|---|---|
| `tests/__init__.py` | Create | Make `tests/` a package so the test runner can find it. |
| `tests/test_debug_enabled.py` | Create | unittest-based regression tests for the `_debug_enabled` helper in both Flask modules; parametrized so the two copies can't drift. |
| `app/cm_web_view.py` | Modify | Add `_debug_enabled()`; replace `debug=True` in `app.run(...)` at line 748. |
| `app/cm_api.py` | Modify | Add `import os`; add `_debug_enabled()`; change `run()` default to `debug=None` with env resolution. |
| `docker-compose.yml` | Modify | Plumb `CM_DEBUG: ${CM_DEBUG:-false}` into `api-server` and `web-view` env blocks. |
| `.env.example` | Modify | New `Runtime` section documenting `CM_DEBUG`. |
| `AGENTS.md` | Modify | One-line note about `CM_DEBUG` under Security & Configuration Tips. |
The `_debug_enabled` helper is intentionally duplicated rather than placed in a shared module — only two call sites, the parser is six lines, and `app/__init__.py` is just a package marker. The test file enforces parity.
---
## Task 1: Test infrastructure + first failing test for `cm_web_view._debug_enabled`
**Files:**
- Create: `tests/__init__.py`
- Create: `tests/test_debug_enabled.py`
- [ ] **Step 1: Create the test package marker**
Create `tests/__init__.py` empty:
```python
```
- [ ] **Step 2: Write the failing test**
Create `tests/test_debug_enabled.py`:
```python
"""Regression tests for the _debug_enabled helper.
Both app.cm_api and app.cm_web_view define a private _debug_enabled()
function that parses the CM_DEBUG environment variable. They are
intentionally duplicated (only two call sites; no shared utility module
exists). This test runs the same parametrized cases against every copy
to catch drift if one is updated without the other.
"""
import os
import unittest
from unittest import mock
# Import the modules at top-level (before any mock.patch.dict with
# clear=True), so module-load-time os.getenv() reads see the real
# environment. The patches inside individual tests then only affect the
# helper's runtime read of CM_DEBUG.
import app.cm_api
import app.cm_web_view
# Modules expected to expose a private _debug_enabled() helper.
# Add new entries here if more Flask entrypoints adopt the same toggle.
HELPER_MODULES = (
app.cm_web_view,
app.cm_api,
)
# (env_value, expected_result). env_value=None means CM_DEBUG is unset.
CASES = (
(None, False),
("", False),
("false", False),
("False", False),
("FALSE", False),
("0", False),
("no", False),
("anything-else", False),
("true", True),
("True", True),
("TRUE", True),
("1", True),
("yes", True),
("YES", True),
(" true ", True), # whitespace tolerated
)
class DebugEnabledTests(unittest.TestCase):
def _resolve(self, module):
return getattr(module, "_debug_enabled", None)
def test_helper_exists_on_every_module(self):
for module in HELPER_MODULES:
with self.subTest(module=module.__name__):
helper = self._resolve(module)
self.assertTrue(
callable(helper),
f"{module.__name__}._debug_enabled must be callable",
)
def test_parses_cm_debug_consistently(self):
for module in HELPER_MODULES:
helper = self._resolve(module)
if helper is None:
self.fail(
f"{module.__name__}._debug_enabled is missing — "
"make test_helper_exists_on_every_module pass first"
)
for env_value, expected in CASES:
with self.subTest(module=module.__name__, env=env_value):
env = {} if env_value is None else {"CM_DEBUG": env_value}
with mock.patch.dict(os.environ, env, clear=True):
self.assertEqual(
helper(),
expected,
f"{module.__name__}._debug_enabled() should be "
f"{expected!r} for CM_DEBUG={env_value!r}",
)
if __name__ == "__main__":
unittest.main()
```
- [ ] **Step 3: Run the test to verify it fails**
Run from repo root:
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
.venv/bin/python -m unittest tests.test_debug_enabled -v
```
Expected: import error or `AttributeError: module 'app.cm_web_view' has no attribute '_debug_enabled'`. The failure mode may be that importing `app.cm_web_view` itself fails because Flask is needed — that is fine, see Task 2 for the venv prerequisite.
- [ ] **Step 4: Install runtime deps into the venv if Flask import fails**
Only if the previous step failed at import time with `ModuleNotFoundError: No module named 'flask'`, run:
```bash
/home/yiekheng/projects/cm_bot_v2/.venv/bin/pip install -r /home/yiekheng/projects/cm_bot_v2/requirements.txt
```
Then re-run Step 3. Expected after install: the test fails with the missing `_debug_enabled` attribute, not an import error.
- [ ] **Step 5: Commit the failing test**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add tests/__init__.py tests/test_debug_enabled.py && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "test: add CM_DEBUG helper parity test (failing)"
```
---
## Task 2: Implement `_debug_enabled` in `cm_web_view.py` and flip `app.run`
**Files:**
- Modify: `app/cm_web_view.py` (top of file — verify `import os` already present at line 10; bottom of file — `__main__` block currently at lines 744-748)
- [ ] **Step 1: Verify `os` is already imported**
Run:
```bash
grep -n "^import os" /home/yiekheng/projects/cm_bot_v2/app/cm_web_view.py
```
Expected: `10:import os`. If absent, add `import os` to the imports section. Do not add `from os import getenv``os.getenv` is what the helper uses.
- [ ] **Step 2: Add the helper near the top of the module**
Insert after the imports section (before `app = Flask(...)`). Find the first blank line after the imports and insert:
```python
def _debug_enabled() -> bool:
"""Return True iff CM_DEBUG env var is set to a truthy value.
Truthy: '1', 'true', 'yes' (case-insensitive, whitespace-trimmed).
Anything else, including unset, is False. Default-off so the
Werkzeug debugger is never reachable in production containers.
"""
return os.getenv("CM_DEBUG", "false").strip().lower() in ("1", "true", "yes")
```
- [ ] **Step 3: Replace `debug=True` in the `__main__` block**
Find:
```python
if __name__ == '__main__':
print("Starting CM Web View...")
print("Web interface will be available at: http://localhost:8000")
print("Make sure the API server is running on port 3000")
app.run(host='0.0.0.0', port=8000, debug=True)
```
Replace the last line with:
```python
app.run(host='0.0.0.0', port=8000, debug=_debug_enabled())
```
- [ ] **Step 4: Run the test — `cm_web_view` cases should now pass, `cm_api` cases should still fail**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
.venv/bin/python -m unittest tests.test_debug_enabled -v
```
Expected: `test_helper_exists_on_every_module` and `test_parses_cm_debug_consistently` still fail because `app.cm_api._debug_enabled` doesn't exist yet. The `app.cm_web_view` subTest entries should pass. Confirm by reading the verbose subtest output.
- [ ] **Step 5: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add app/cm_web_view.py && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "feat(web): make Werkzeug debug opt-in via CM_DEBUG"
```
---
## Task 3: Implement `_debug_enabled` in `cm_api.py` and switch `run()` to env-resolved default
**Files:**
- Modify: `app/cm_api.py` (top of file — currently no `import os`; class `CM_API.run` method around line 160)
- [ ] **Step 1: Add `import os` at the top**
Find the existing import block (currently lines 1-4):
```python
import threading
from flask import Flask, jsonify, request
from flask_cors import CORS
from .db import DB
```
Insert `import os` as the very first line so the imports remain stdlib-then-third-party:
```python
import os
import threading
from flask import Flask, jsonify, request
from flask_cors import CORS
from .db import DB
```
- [ ] **Step 2: Add the helper above the `CM_API` class**
Insert between the imports and `class CM_API:` (around line 6):
```python
def _debug_enabled() -> bool:
"""Return True iff CM_DEBUG env var is set to a truthy value.
Truthy: '1', 'true', 'yes' (case-insensitive, whitespace-trimmed).
Anything else, including unset, is False. Default-off so the
Werkzeug debugger is never reachable in production containers.
"""
return os.getenv("CM_DEBUG", "false").strip().lower() in ("1", "true", "yes")
```
This text is identical to the helper in `cm_web_view.py` — the parametrized test enforces it stays that way.
- [ ] **Step 3: Change the `run` method signature and resolve when unset**
Find the existing method (currently around line 160):
```python
def run(self, port=3000, debug=True):
# Test database connection before starting server
test_db = self._get_database_connection()
if test_db is None:
print("Cannot start server: Database not available")
exit(1)
self._close_database_connection(test_db)
print(f'CM Bot DB API Listening at Port : {port}')
self.app.run(host='0.0.0.0', port=port, debug=debug)
```
Replace with:
```python
def run(self, port=3000, debug=None):
if debug is None:
debug = _debug_enabled()
# Test database connection before starting server
test_db = self._get_database_connection()
if test_db is None:
print("Cannot start server: Database not available")
exit(1)
self._close_database_connection(test_db)
print(f'CM Bot DB API Listening at Port : {port}')
self.app.run(host='0.0.0.0', port=port, debug=debug)
```
Do **not** touch `run_in_thread` — its `debug=False` default is already safe and used internally.
- [ ] **Step 4: Run the test — both modules should now pass**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
.venv/bin/python -m unittest tests.test_debug_enabled -v
```
Expected: `OK` with all subTests passing for both `app.cm_web_view` and `app.cm_api`.
- [ ] **Step 5: Confirm no caller passes `debug` positionally to `run()`**
```bash
grep -rn "\.run(" /home/yiekheng/projects/cm_bot_v2/app/ | grep -v "self\.app\.run\|app\.run("
```
Expected: only `api.run(port = 3000)` in `cm_api.py:191`. If anything else appears that passes a positional second arg to `CM_API.run`, change it to a keyword argument before continuing — the signature change went from `debug=True` to `debug=None`, so a caller passing `True` positionally would now incorrectly enable debug.
- [ ] **Step 6: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add app/cm_api.py && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "feat(api): make Werkzeug debug opt-in via CM_DEBUG"
```
---
## Task 4: Plumb `CM_DEBUG` into `docker-compose.yml`
**Files:**
- Modify: `docker-compose.yml` (the `api-server` `environment:` block at lines 40-49 and the `web-view` `environment:` block at lines 63-66)
- [ ] **Step 1: Add `CM_DEBUG` to the `api-server` environment**
Find:
```yaml
environment:
PYTHONUNBUFFERED: "1"
DB_HOST: ${DB_HOST}
```
(under `api-server:`, currently lines 40-42). Insert `CM_DEBUG` directly after `PYTHONUNBUFFERED`:
```yaml
environment:
PYTHONUNBUFFERED: "1"
CM_DEBUG: ${CM_DEBUG:-false}
DB_HOST: ${DB_HOST}
```
The `${CM_DEBUG:-false}` form guarantees the variable is defined inside the container even if the operator did not set it in their `.env`.
- [ ] **Step 2: Add `CM_DEBUG` to the `web-view` environment**
Find (under `web-view:`, currently lines 63-66):
```yaml
environment:
PYTHONUNBUFFERED: "1"
API_BASE_URL: http://api-server:3000
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
```
Insert `CM_DEBUG` after `PYTHONUNBUFFERED`:
```yaml
environment:
PYTHONUNBUFFERED: "1"
CM_DEBUG: ${CM_DEBUG:-false}
API_BASE_URL: http://api-server:3000
CM_PREFIX_PATTERN: ${CM_PREFIX_PATTERN}
```
Do **not** add it to `telegram-bot` or `transfer-bot` — neither runs a Flask server.
- [ ] **Step 3: Validate the compose file parses**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
docker compose -f docker-compose.yml config >/dev/null && echo OK
```
Expected: `OK`. If the command errors with a YAML or interpolation problem, fix the indentation around the new lines.
- [ ] **Step 4: Confirm the variable reaches both services in the rendered config**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
docker compose -f docker-compose.yml config | grep -E "CM_DEBUG"
```
Expected: two matching lines, one each under `api-server` and `web-view`, value rendered as `false` (or whatever `CM_DEBUG` resolves to in the current shell env).
- [ ] **Step 5: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add docker-compose.yml && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "chore(compose): pass CM_DEBUG into api-server and web-view"
```
---
## Task 5: Document `CM_DEBUG` in `.env.example`
**Files:**
- Modify: `.env.example` (currently 32 lines; the existing `=== Deployment Identity ===` section is at the top)
- [ ] **Step 1: Insert a new `Runtime` section near the top**
Find the first three lines of `.env.example`:
```
# === Deployment Identity ===
# Unique name prefix for containers and network (avoid conflicts on same host)
CM_DEPLOY_NAME=rex-cm
```
Add a new section above them:
```
# === Runtime ===
# Set to true ONLY in local dev. Werkzeug debugger = RCE if exposed.
CM_DEBUG=false
# === Deployment Identity ===
# Unique name prefix for containers and network (avoid conflicts on same host)
CM_DEPLOY_NAME=rex-cm
```
- [ ] **Step 2: Confirm the file still parses as a valid env file**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
grep -E "^[A-Z_]+=" .env.example | wc -l
```
Expected: one more line than before the change (run before/after to verify if curious; the absolute count is implementation-dependent).
- [ ] **Step 3: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add .env.example && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "docs(env): document CM_DEBUG in .env.example"
```
---
## Task 6: Add a one-line note to `AGENTS.md`
**Files:**
- Modify: `AGENTS.md` (the `## Security & Configuration Tips` section at the bottom)
- [ ] **Step 1: Append a bullet to the Security section**
Find the existing section (currently the last block in the file):
```
## Security & Configuration Tips
- Never commit real secrets in `.env`.
- `app/cm_bot_hal.py` currently contains hardcoded agent credentials/pin; move these to env vars before production use.
- Keep container clocks mounted (`/etc/timezone`, `/etc/localtime`) as compose currently defines to avoid schedule drift.
```
Add a new bullet above the `cm_bot_hal.py` line:
```
## Security & Configuration Tips
- Never commit real secrets in `.env`.
- `CM_DEBUG` defaults to `false` for both Flask services. Set it to `true` only in local development; rex/siong production env files must leave it unset (the Werkzeug debugger is RCE if reachable).
- `app/cm_bot_hal.py` currently contains hardcoded agent credentials/pin; move these to env vars before production use.
- Keep container clocks mounted (`/etc/timezone`, `/etc/localtime`) as compose currently defines to avoid schedule drift.
```
- [ ] **Step 2: Commit**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
git add AGENTS.md && \
git -c user.name='yiekheng' -c user.email='yiekheng@04080616.xyz' \
commit -m "docs(agents): note CM_DEBUG default and intent"
```
---
## Task 7: Integration verification (manual run)
This task corresponds to the four verification scenarios in the spec. No commit — these are smoke checks. If any fails, do not declare done; debug and fix.
**Files:** none modified.
- [ ] **Step 1: Local dev with `CM_DEBUG=true` — debug ON path**
In a scratch repo-root `.env` (or temporarily setting in shell), set:
```
CM_DEBUG=true
```
Then bring up the local stack:
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
docker compose -f docker-compose.yml -f docker-compose.override.yml up --build -d api-server web-view
```
Inspect logs:
```bash
docker compose logs --no-color api-server web-view | grep -E "Debug mode|Debugger PIN"
```
Expected: at least one `* Debug mode: on` line and at least one `Debugger PIN:` line.
Tear down:
```bash
docker compose down
```
- [ ] **Step 2: Local dev with `CM_DEBUG=false` (default) — debug OFF path**
Unset or set `CM_DEBUG=false` in `.env`, then:
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
docker compose -f docker-compose.yml -f docker-compose.override.yml up --build -d api-server web-view
```
Inspect logs:
```bash
docker compose logs --no-color api-server web-view | grep -E "Debug mode|Debugger PIN" || echo "no debug lines (expected)"
```
Expected: prints `no debug lines (expected)` because neither pattern matches.
Sanity-check both endpoints still serve:
```bash
curl -s -o /dev/null -w "api %{http_code}\n" http://localhost:3000/acc/ ; \
curl -s -o /dev/null -w "web %{http_code}\n" "http://localhost:${CM_WEB_HOST_PORT:-8001}/api/acc/"
```
Expected: `api 200` and `web 200` (assuming the database is reachable; if the api shows 5xx because of DB, that is unrelated to this change — the absence of debug lines above is the success criterion for this task).
Tear down:
```bash
docker compose down
```
- [ ] **Step 3: Override path regression check**
From the repo root:
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
.venv/bin/python -c "
import os
os.environ['CM_DEBUG'] = 'false'
from app.cm_api import _debug_enabled
print('helper says:', _debug_enabled())
import inspect
sig = inspect.signature(__import__('app.cm_api', fromlist=['CM_API']).CM_API.run)
print('run() signature:', sig)
"
```
Expected output:
```
helper says: False
run() signature: (self, port=3000, debug=None)
```
- [ ] **Step 4: Re-run the full unit test suite**
```bash
cd /home/yiekheng/projects/cm_bot_v2 && \
.venv/bin/python -m unittest tests.test_debug_enabled -v
```
Expected: `OK` with all subTests passing.
---
## Spec Coverage Check (self-review)
| Spec requirement | Task |
|---|---|
| `_debug_enabled()` helper definition | Task 2 (web), Task 3 (api) |
| `cm_web_view.py:748` debug flag swap | Task 2 |
| `cm_api.py:160` signature change to `debug=None` | Task 3 |
| `import os` added to `cm_api.py` | Task 3, Step 1 |
| `run_in_thread` left untouched | Task 3, Step 3 (explicit "Do not touch") |
| `docker-compose.yml` plumbing for both Flask services | Task 4 |
| `.env.example` Runtime section | Task 5 |
| `AGENTS.md` one-line note | Task 6 |
| Verification: debug-on local | Task 7, Step 1 |
| Verification: debug-off local + endpoints serve | Task 7, Step 2 |
| Verification: explicit override path | Task 7, Step 3 |
| Regression test for helper parity | Task 1 (write), Tasks 2 & 3 (make pass), Task 7 Step 4 (re-run) |