Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.260 — Release IB (stage-r8) Un-held safety fixes (author resolved my earlier hold findings; re-reviewed fresh) + a clean fix batch. 6 PRs. ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3535 (#3538) | @rodboev | **Self-update recovers from a stash-pop conflict without data loss.** Was a BRICK bug (`git reset --merge` + `git stash drop` discarded local mods while reporting success). Now keeps the stash, returns `ok:false` + "preserved in `stash@{0}`", no restart on conflict. *(was held — fix verified)* | | #1909 s3 (#3562) | @rodboev | **Auth `Secure` cookie no longer locks out plain-HTTP LAN/Tailscale users.** Secure now keys only on real TLS evidence (env / TLS socket / opt-in `TRUST_FORWARDED_PROTO`); non-loopback plain-HTTP is no longer force-Secure. SameSite back to `Lax`. *(was held — fix verified)* | | #2785 (#3559) | @franksong2702 | Clearer cron/gateway diagnostics for single-container Docker (gateway configured, no daemon → jobs silently don't fire). | | #3555 | @lambyangzhao | Long TTS responses chunked at sentence boundaries (works around the browser's ~32K silent-truncation). | | #3340 (#3342) | @rly09 | Persistent-state toast when a turn has saved memory / created-updated a skill. | | #3533 | @franksong2702 | `/reload-mcp` marked `cli_only` so the WebUI doesn't dispatch it as an LLM prompt. | ### Gate - Full pytest suite: **7681 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — confirmed the stash-conflict path never drops the stash / never restarts on conflict, auth Secure handles LAN-HTTP correctly with no header-forgery hole, `/reload-mcp` allowlisted, state-toast has a real backend writer + active-session guard, diagnostics leak no paths, TTS chunking preserves order. Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> Co-authored-by: lambyangzhao <lambyangzhao@users.noreply.github.com> Co-authored-by: rly09 <rly09@users.noreply.github.com>
268 lines
11 KiB
Python
268 lines
11 KiB
Python
"""Regression coverage: /api/gateway/status uses agent_health payload as
|
|
the authoritative 'running' signal (#d0568682 / parent review t_9098e3db).
|
|
|
|
Before the fix, the handler called gateway.status.get_running_pid() directly
|
|
and fell back to bool(identity_map) when the module was unavailable. The fix
|
|
makes it consult agent_health.build_agent_health_payload() so the tri-state
|
|
`alive` field is the single source of truth for gateway process health.
|
|
|
|
Tests use handle_get + monkeypatched build_agent_health_payload() and
|
|
_load_gateway_session_identity_map() to isolate the gateway status route
|
|
from real filesystem state.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
# ── FakeHandler (mirrors test_1560_password_env_var_no_op._FakeHandler) ────────
|
|
|
|
class _FakeHandler:
|
|
"""Minimal BaseHTTPRequestHandler stand-in for routes.handle_get."""
|
|
|
|
def __init__(self):
|
|
self.status = None
|
|
self.sent_headers: list[tuple[str, str]] = []
|
|
self.body = bytearray()
|
|
self.wfile = self
|
|
|
|
def send_response(self, code):
|
|
self.status = code
|
|
|
|
def send_header(self, key, value):
|
|
self.sent_headers.append((key, value))
|
|
|
|
def end_headers(self):
|
|
pass
|
|
|
|
def write(self, data):
|
|
"""Accumulate bytes written to wfile."""
|
|
self.body.extend(data if isinstance(data, (bytes, bytearray)) else data.encode("utf-8"))
|
|
|
|
def get_json(self):
|
|
"""Parse the accumulated body as JSON."""
|
|
return json.loads(self.body.decode("utf-8"))
|
|
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
def _call_gateway_status(monkeypatch, agent_health_alive, identity_map=None, details=None):
|
|
"""Invoke handle_get for /api/gateway/status and return the parsed JSON.
|
|
|
|
monkeypatches build_agent_health_payload to return the given `alive` value
|
|
and _load_gateway_session_identity_map to return the given identity_map.
|
|
"""
|
|
from api import routes
|
|
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"build_agent_health_payload",
|
|
lambda: {
|
|
"alive": agent_health_alive,
|
|
"checked_at": "2026-05-06T12:00:00+00:00",
|
|
"details": details or {},
|
|
},
|
|
)
|
|
|
|
if identity_map is not None:
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_load_gateway_session_identity_map",
|
|
lambda: identity_map,
|
|
)
|
|
|
|
handler = _FakeHandler()
|
|
parsed = urlparse("http://example.com/api/gateway/status")
|
|
routes.handle_get(handler, parsed)
|
|
return handler.get_json()
|
|
|
|
|
|
# ── Acceptance criteria tests ─────────────────────────────────────────────────
|
|
|
|
def test_gateway_status_running_true_when_agent_health_alive_and_no_sessions(monkeypatch):
|
|
"""AC1: alive=true + empty identity_map → running=true, configured=true, platforms=[]"""
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=True, identity_map={})
|
|
assert result["running"] is True
|
|
assert result["configured"] is True
|
|
assert result["platforms"] == []
|
|
|
|
|
|
def test_gateway_status_running_false_when_agent_health_alive_false_and_no_sessions(monkeypatch):
|
|
"""AC2: alive=false + empty identity_map → running=false, configured=true, platforms=[]"""
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=False, identity_map={})
|
|
assert result["running"] is False
|
|
assert result["configured"] is True
|
|
assert result["platforms"] == []
|
|
|
|
|
|
def test_gateway_status_running_false_when_agent_health_alive_none_and_no_sessions(monkeypatch):
|
|
"""When alive=None (not configured): fall back to identity_map heuristic,
|
|
and set configured=false so frontend can show 'not configured' state."""
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=None, identity_map={})
|
|
assert result["running"] is False
|
|
assert result["configured"] is False
|
|
assert result["platforms"] == []
|
|
|
|
|
|
def test_gateway_status_running_true_and_platforms_when_agent_health_alive_and_sessions(monkeypatch):
|
|
"""AC3: alive=true + sessions with platforms → running=true, configured=true, platforms populated"""
|
|
identity_map = {
|
|
"sess_a": {"raw_source": "telegram", "platform": "telegram"},
|
|
"sess_b": {"raw_source": "discord", "platform": "discord"},
|
|
}
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=True, identity_map=identity_map)
|
|
assert result["running"] is True
|
|
assert result["configured"] is True
|
|
assert len(result["platforms"]) == 2
|
|
names = {p["name"] for p in result["platforms"]}
|
|
assert names == {"telegram", "discord"}
|
|
|
|
|
|
# ── Edge case tests ───────────────────────────────────────────────────────────
|
|
|
|
def test_gateway_status_alive_none_falls_back_to_identity_map_heuristic(monkeypatch):
|
|
"""When alive=None (not configured) but sessions exist, running reflects identity_map.
|
|
configured=true because sessions metadata proves a gateway is configured
|
|
even if gateway.status cannot be imported in this WebUI process."""
|
|
from api import routes
|
|
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"build_agent_health_payload",
|
|
lambda: {"alive": None, "checked_at": "2026-05-06T12:00:00+00:00", "details": {}},
|
|
)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_load_gateway_session_identity_map",
|
|
lambda: {"sess_c": {"raw_source": "telegram", "platform": "telegram"}},
|
|
)
|
|
|
|
handler = _FakeHandler()
|
|
parsed = urlparse("http://example.com/api/gateway/status")
|
|
routes.handle_get(handler, parsed)
|
|
result = handler.get_json()
|
|
# Fallback to identity_map: sessions exist → running=true
|
|
assert result["running"] is True
|
|
# Existing gateway sessions prove the gateway has been configured.
|
|
assert result["configured"] is True
|
|
|
|
|
|
def test_gateway_status_handles_corrupted_sessions_json(monkeypatch):
|
|
"""Edge: sessions.json is corrupted → identity_map empty, rely on agent_health alone."""
|
|
from api import routes
|
|
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"build_agent_health_payload",
|
|
lambda: {"alive": True, "checked_at": "2026-05-06T12:00:00+00:00", "details": {}},
|
|
)
|
|
# _load_gateway_session_identity_map already returns {} on JSON parse failure;
|
|
# we monkeypatch it to return {} to simulate corrupted file.
|
|
monkeypatch.setattr(routes, "_load_gateway_session_identity_map", lambda: {})
|
|
|
|
handler = _FakeHandler()
|
|
parsed = urlparse("http://example.com/api/gateway/status")
|
|
routes.handle_get(handler, parsed)
|
|
result = handler.get_json()
|
|
assert result["running"] is True
|
|
assert result["platforms"] == []
|
|
assert result["session_count"] == 0
|
|
|
|
|
|
def test_gateway_status_blank_platform_fields_empty_platforms_running_true(monkeypatch):
|
|
"""Edge: sessions exist but all have blank/missing platform fields → platforms=[], running=true."""
|
|
from api import routes
|
|
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"build_agent_health_payload",
|
|
lambda: {"alive": True, "checked_at": "2026-05-06T12:00:00+00:00", "details": {}},
|
|
)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_load_gateway_session_identity_map",
|
|
lambda: {
|
|
"sess_d": {"raw_source": "", "platform": ""},
|
|
"sess_e": {}, # no platform field at all
|
|
},
|
|
)
|
|
|
|
handler = _FakeHandler()
|
|
parsed = urlparse("http://example.com/api/gateway/status")
|
|
routes.handle_get(handler, parsed)
|
|
result = handler.get_json()
|
|
assert result["running"] is True
|
|
assert result["platforms"] == []
|
|
|
|
|
|
# ── Existing behavior preservation tests ──────────────────────────────────────
|
|
|
|
def test_gateway_status_running_false_when_agent_health_down_even_with_sessions(monkeypatch):
|
|
"""When agent_health says alive=false, running should be false regardless of sessions."""
|
|
from api import routes
|
|
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"build_agent_health_payload",
|
|
lambda: {"alive": False, "checked_at": "2026-05-06T12:00:00+00:00", "details": {}},
|
|
)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_load_gateway_session_identity_map",
|
|
lambda: {"sess_f": {"raw_source": "telegram", "platform": "telegram"}},
|
|
)
|
|
|
|
handler = _FakeHandler()
|
|
parsed = urlparse("http://example.com/api/gateway/status")
|
|
routes.handle_get(handler, parsed)
|
|
result = handler.get_json()
|
|
# Running should be false even though sessions exist — agent_health is authoritative
|
|
assert result["running"] is False
|
|
# But configured=true because alive=False means gateway metadata exists
|
|
assert result["configured"] is True
|
|
# But platforms should still be extracted from sessions
|
|
assert len(result["platforms"]) == 1
|
|
assert result["platforms"][0]["name"] == "telegram"
|
|
|
|
|
|
def test_gateway_status_missing_r_field_handled_by_frontend(monkeypatch):
|
|
"""Edge: response always has 'running' and 'configured' fields.
|
|
Frontend handles missing field via catch block. This test verifies the backend
|
|
always includes both fields in responses."""
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=True, identity_map={})
|
|
assert "running" in result
|
|
assert "configured" in result
|
|
|
|
|
|
def test_gateway_status_includes_gateway_health_metadata(monkeypatch):
|
|
"""Expose gateway health reason/state metadata so the UI can render better diagnostics."""
|
|
result = _call_gateway_status(
|
|
monkeypatch,
|
|
agent_health_alive=None,
|
|
identity_map={},
|
|
details={
|
|
"state": "unknown",
|
|
"reason": "gateway_stale_running_state",
|
|
"gateway_state": "running",
|
|
},
|
|
)
|
|
assert result["health"] == {
|
|
"state": "unknown",
|
|
"reason": "gateway_stale_running_state",
|
|
"gateway_state": "running",
|
|
}
|
|
|
|
|
|
def test_gateway_status_last_active_empty_when_alive_and_no_sessions_path(monkeypatch):
|
|
"""Bonus: alive=true + identity_map={} → last_active is empty string.
|
|
This guards the 'if running and sessions_path.exists()' guard from being
|
|
silently removed in a future refactor that might expose a stale timestamp."""
|
|
result = _call_gateway_status(monkeypatch, agent_health_alive=True, identity_map={})
|
|
assert result["running"] is True
|
|
assert result["configured"] is True
|
|
# In test context, sessions_path won't exist (no real filesystem),
|
|
# so last_active must be empty.
|
|
assert result["last_active"] == ""
|