phase: 33_gateway_route_switching

Phase 33 completion pass is done. Final report:

## Phase 33 — completion report

**All 8 task files complete** (`.agents/phases/complete/33_gateway_route_switching/`), full gate green, docs verified.

### Work in this pass

1. **`tests/test_routectl.py` added** (17 tests, all green) — exercises the *real* helper binary unprivileged against a fake tree (`--root`), the real agent parser, the captured gateway fixtures, and stubbed `nginx`/`haproxy`/`systemctl` on `PATH`. Pins the full protocol (locate → state → dry-run → temp-file/fsync/`os.replace` write → checker → restore on checker failure → reload-never-restart → re-read proof → sidecar record), all three route shapes (haproxy `server`, nginx `proxy_pass`, nginx stream `map` via its sink-value id), `drift` / unknown-id / `map-default` refusals, `list`/`verify`, and the `--serve` peer gate (`SO_PEERCRED` uid check, one JSON line per request, exact body shape).

2. **Real helper defect fixed** — `deploy/dashboards-routectl.py`: `main()`'s early-return paths returned `({"ok": False, …}, EXIT_*)` tuples, so `sys.exit()` printed a Python repr instead of a JSON line (usage errors, agent-import refusal). Now prints `json.dumps(…)` and returns only the code, keeping the one-JSON-line contract.

3. **Documentation consistency fixes** (task 08 criterion 6):
   - `README.md`: garbled verdict-colour sentence on the switch ("green and to the position") → "in the **on** position and tinted `--gate-on`" (the existence-pair contract).
   - `README.md`: audit sentence now names all three event kinds — `route-disabled` / `route-enabled` / **`route-denied`** (the code stores one per refused call; dry runs store none).
   - `page-gateway.js`: replaced the stale pre-33 module docstring ("the switch is not wired") with the two-arm `GATEWAY_ALLOW_WRITES` contract.
   - `AGENTS.md`: every phase-33 claim re-verified against code (the ten gates, the 35 s/30 s budget pair, `gateway.writes_enabled`, `/api/summary`'s `disabled`, the 1.4.0 agent bullet, the gap list) — no changes needed.

### Verification
- `uv run pytest --cov`: **1970 passed**, 18 skipped — coverage **98.74%** (floor 94%)
- `ruff` clean · `pyright` 0 errors · `bash .agents/validate.sh` **exit 0** (incl. `node --check` on every page script)
- Dev server running; all 10 sources `ok`; bundles exported

### Live infrastructure
**No live changes made.** The live drill remains unexecuted per `data/logs/NOTES-33-switching.md` (no operator approval; checker binaries unavailable in the podman proxy units). All routes stay enabled, config hashes unchanged, and the sandbox rehearsal + backup tree are in place for the operator's own `DANGER` call. All changes are in the working tree only.
This commit is contained in:
2026-11-01 11:50:51 -05:00
parent 0271387053
commit 86c8bf4992
91 changed files with 7568 additions and 519 deletions
+39
View File
@@ -135,6 +135,45 @@ def test_the_gateway_settings_come_from_the_environment(monkeypatch: pytest.Monk
importlib.reload(config)
def test_the_gateway_write_is_off_by_default_and_budgets_are_their_own() -> None:
"""Phase 33 task 05: the flag is a *decision*, and its default is the safe one.
`GATEWAY_ALLOW_WRITES` is `NUT_ALLOW_ACTIONS`'s sibling: the same truthy rule, the same
default, and its own budget. The read's 10 s is the read's alone; the write's 35 s default
is the write's own, and the two never share a number.
"""
settings = Settings()
assert settings.gateway_allow_writes is False
assert settings.gateway_write_timeout == 35.0
assert settings.gateway_timeout == 10.0
def test_the_gateway_write_settings_come_from_the_environment(
monkeypatch: pytest.MonkeyPatch) -> None:
"""`GATEWAY_ALLOW_WRITES=1` is the only way the flag turns on, the same rule as the NUT one."""
monkeypatch.setenv("GATEWAY_ALLOW_WRITES", "1")
monkeypatch.setenv("GATEWAY_WRITE_TIMEOUT_SECONDS", "12.5")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.gateway_allow_writes is True
assert reloaded.SETTINGS.gateway_write_timeout == 12.5
finally:
for name in ("GATEWAY_ALLOW_WRITES", "GATEWAY_WRITE_TIMEOUT_SECONDS"):
monkeypatch.delenv(name)
importlib.reload(config)
def test_the_gateway_flag_uses_the_same_truthy_rule_as_every_other_flag(
monkeypatch: pytest.MonkeyPatch) -> None:
"""The rule is `_flag`'s: the two writes are two decisions, the same shape (task 05)."""
for truthy in ("1", "true", "TRUE", "yes", "on"):
monkeypatch.setenv("GATEWAY_ALLOW_WRITES", truthy)
assert config._flag("GATEWAY_ALLOW_WRITES") is True, truthy
for falsey in ("0", "false", "", "no"):
monkeypatch.setenv("GATEWAY_ALLOW_WRITES", falsey)
assert config._flag("GATEWAY_ALLOW_WRITES") is False, falsey
def test_the_disk_source_defaults_to_a_five_minute_cadence() -> None:
"""SMART is not a per-second signal (phase 09), and the fetch waits for the agent's
own 20 s smartctl budget. Both are env-only, like every other interval."""