From a09195b1e8d232e8c83b5430da4261970f62a820 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:26:28 -0700 Subject: [PATCH] fix(messaging): dual-live and cross-profile invariants for the served-profile card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widen the #112765 regression (from #112784, @liuhao1024) to the two cases the three community fixes split on: - a sibling served profile with no ``:telegram`` entry in the shared record must not inherit another profile's connected verdict (from #112786, @kvnloo); - the transitional dual-live case — a profile running its own standalone gateway while the live multiplexer still lists it in ``served_profiles`` — keeps the own record, the same rung order ``resolve_gateway_liveness`` uses, so liveness and platform state never come from two different gateways (from #112790, @kokhlo). Red on the "multiplexer always wins" shape, green on the precedence-mirroring one. Also: every ``write_text`` in the file carries ``encoding="utf-8"`` (windows-footgun gate). --- .../test_served_profile_mirror_platforms.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/tests/hermes_cli/test_served_profile_mirror_platforms.py b/tests/hermes_cli/test_served_profile_mirror_platforms.py index d101ecdd18..a081f6b59c 100644 --- a/tests/hermes_cli/test_served_profile_mirror_platforms.py +++ b/tests/hermes_cli/test_served_profile_mirror_platforms.py @@ -18,8 +18,8 @@ import pytest def served_root(tmp_path, monkeypatch): root = tmp_path / "hermes" (root / "profiles" / "alpha").mkdir(parents=True) - (root / "config.yaml").write_text("gateway: {multiplex_profiles: true}\n") - (root / "gateway.pid").write_text(json.dumps({"pid": os.getpid(), "hermes_home": str(root)})) + (root / "config.yaml").write_text("gateway: {multiplex_profiles: true}\n", encoding="utf-8") + (root / "gateway.pid").write_text(json.dumps({"pid": os.getpid(), "hermes_home": str(root)}), encoding="utf-8") (root / "gateway_state.json").write_text(json.dumps({ "pid": os.getpid(), "hermes_home": str(root), "gateway_state": "running", "served_profiles": ["default", "alpha", "beta"], @@ -27,7 +27,7 @@ def served_root(tmp_path, monkeypatch): "api_server": {"state": "connected", "listener_base": "http://127.0.0.1:45719"}, "webhook": {"state": "fatal", "error_code": "port_in_use"}, "alpha:telegram": {"state": "connected"}, - }})) + }}), encoding="utf-8") monkeypatch.setenv("HERMES_HOME", str(root)) monkeypatch.delenv("GATEWAY_MULTIPLEX_PROFILES", raising=False) import hermes_constants @@ -77,7 +77,41 @@ def test_messaging_card_ignores_a_served_profiles_stale_own_runtime_record(serve "required_env": []} alpha = served_root / "profiles" / "alpha" (alpha / "gateway_state.json").write_text(json.dumps( - {"gateway_state": "stopped", "platforms": {}})) + {"gateway_state": "stopped", "platforms": {}}), encoding="utf-8") [payload] = messaging._platform_payloads(alpha, [entry]) assert payload["gateway_running"] is True assert payload["state"] == "connected", payload + # The shared record is scoped per profile: beta has no ``beta:telegram`` entry, so alpha's + # connected verdict must not bleed into beta's card. + beta = served_root / "profiles" / "beta" + beta.mkdir() + (beta / "gateway_state.json").write_text(json.dumps({"gateway_state": "stopped", "platforms": {}}), encoding="utf-8") + [beta_payload] = messaging._platform_payloads(beta, [entry]) + assert beta_payload["state"] == "pending_restart", beta_payload + + +def test_messaging_card_keeps_a_live_own_gateway_record_over_the_multiplexer(served_root, monkeypatch): + """Transitional dual-live case: a profile running its own standalone gateway while the live + multiplexer still lists it in ``served_profiles``. ``resolve_gateway_liveness`` answers from the + own record (rung 3) before the multiplexer (rung 4); the card must read the same record, or + liveness and platform state come from two different gateways.""" + import gateway.status as status + from hermes_cli.web_routers import messaging + monkeypatch.setattr(messaging, "_platform_enablement", lambda *a, **k: (True, True, None)) + alpha = served_root / "profiles" / "alpha" + # Two live gateways: this process is the default multiplexer; a second (fake, never signalled) + # PID wears alpha's argv. Only the live guard sees a foreign PID, so existence is stubbed. + own_pid = 2 ** 22 - 1 + (alpha / "gateway_state.json").write_text(json.dumps({ + "pid": own_pid, "hermes_home": str(alpha), "gateway_state": "running", + "platforms": {"telegram": {"state": "retrying", "error_code": "network"}}}), encoding="utf-8") + real_pid_exists = status._pid_exists + monkeypatch.setattr(status, "_pid_exists", lambda pid: pid == own_pid or real_pid_exists(pid)) + monkeypatch.setattr(status, "_read_process_cmdline", + lambda pid: "hermes -p alpha gateway run" if pid == own_pid else "hermes gateway run") + assert status.multiplexer_liveness_for_profile(alpha) is not None + entry = {"id": "telegram", "name": "Telegram", "description": "", "docs_url": "", "env_vars": [], + "required_env": []} + [payload] = messaging._platform_payloads(alpha, [entry]) + assert payload["gateway_running"] is True + assert payload["state"] == "retrying", payload