fix(gateway): a profile rescan drops status verdicts about removed credentials

Deleting a duplicated bot token from a secondary profile's .env triggers a
rescan, but the old '<profile>:<platform>' fatal entry stayed in
gateway_state.json until restart, so Desktop kept painting 'Backend
telegram down'. Before rebuilding a changed profile, drop its entries for
platforms with no live or queued adapter; the rescan re-publishes every
verdict that still holds (unserved-WhatsApp notes are un-deduped for it).
This commit is contained in:
Teknium committed 2026-10-31 22:25:09 -07:00
1 parent f92db3b007
commit 61ba1f684e
3 files changed
+60 -3

No files matched your search

+15
View File
@@ -273,6 +273,8 @@ class GatewayProfileReconcileMixin:
# Only acknowledge the configuration observed before connecting;
# a setup save during an awaited handshake needs another scan.
scan_signature = profile_serve_signature(current[name])
if name in changed:
self._drop_stale_platform_verdicts(name)
try:
connected = await self._start_one_profile_adapters(name, current[name], claimed)
except MultiplexConfigError as exc:
@@ -328,6 +330,19 @@ class GatewayProfileReconcileMixin:
result["served_profiles"] = self.served_profile_names()
return result
def _drop_stale_platform_verdicts(self, name: str) -> None:
"""Before a changed profile's rescan, drop its ``<name>:<platform>`` status entries for
platforms with no live or queued adapter: the rescan re-publishes every verdict that still
holds, so a fatal/disabled entry about a credential the user just removed can't outlive it."""
from gateway.run import _write_runtime_status_quiet
live = set((getattr(self, "_profile_adapters", None) or {}).get(name) or {})
live |= set((getattr(self, "_profile_failed_platforms", None) or {}).get(name) or {})
noted = getattr(self, "_unserved_secondary_platforms", None)
if isinstance(noted, set):
noted -= {entry for entry in noted if entry[0] == name}
_write_runtime_status_quiet(drop_profile_platforms=name,
keep_platforms={f"{name}:{getattr(p, 'value', p)}" for p in live})
def _parked_profile_names(self) -> list:
"""Profiles that exist but could not be served (unusable store, home owned elsewhere); boot's
``initialize_gateway_runtime`` parks into the same published ``name -> reason`` map."""
+3 -3
View File
@@ -19,7 +19,7 @@ import time
from dataclasses import dataclass
from datetime import datetime, timezone, UTC
from pathlib import Path
from typing import Any, Callable, NamedTuple, Optional
from typing import Any, Callable, Iterable, NamedTuple, Optional
from hermes_dir_acl import make_user_dir
from gateway.status_inline_source import (
@@ -1093,7 +1093,7 @@ def _prepare_runtime_status_update(
parked_profiles: Any = _UNSET, multiplex_standalone_reason: Any = _UNSET,
platform_metrics: Any = _UNSET,
ingress_url: Any = _UNSET, listener_base: Any = _UNSET, clear_profile_platforms: bool = False,
drop_profile_platforms: Optional[str] = None,
drop_profile_platforms: Optional[str] = None, keep_platforms: Iterable[str] = (),
load_existing: bool = True, reload_existing: bool = False,
) -> tuple[Path, dict[str, Any], dict[str, Any]]:
"""Merge one update into the process-wide canonical status snapshot."""
@@ -1117,7 +1117,7 @@ def _prepare_runtime_status_update(
payload["platforms"] = {
k: v for k, v in payload["platforms"].items()
if not isinstance(k, str) or ":" not in k
or (drop_prefix is not None and not k.startswith(drop_prefix))
or (drop_prefix is not None and not k.startswith(drop_prefix)) or k in keep_platforms
}
payload.update({key: current_record[key] for key in ("kind", "pid", "argv", "start_time")})
payload["updated_at"] = _utc_now_iso()
@@ -0,0 +1,42 @@
"""A changed profile's rescan drops status verdicts about credentials the user removed.
Removing a duplicated bot token from a secondary profile's .env must clear its
``<profile>:<platform>`` fatal entry (Desktop: "Backend telegram down"), while a
platform that is still live keeps its entry.
"""
import json
from unittest.mock import patch
import pytest
from gateway.config import Platform
from gateway.status import flush_runtime_status, publish_runtime_status
from tests.gateway.test_multiplex_hot_serve import _mkprofile, _runner
def _platforms(home):
flush_runtime_status()
return json.loads((home / "gateway_state.json").read_text(encoding="utf-8")).get("platforms") or {}
@pytest.mark.asyncio
async def test_rescan_drops_fatal_for_removed_credential_keeps_live(tmp_path, monkeypatch):
runner, home = _runner(tmp_path, monkeypatch)
alpha = _mkprofile(home, "alpha", "DISCORD_BOT_TOKEN=alpha-token\n")
with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"):
await runner._start_secondary_profile_adapters()
assert Platform.DISCORD in runner._profile_adapters["alpha"]
publish_runtime_status(platform="alpha:discord", platform_state="connected")
publish_runtime_status(platform="alpha:telegram", platform_state="fatal",
error_code="duplicate_credential", error_message="dup")
publish_runtime_status(platform="telegram", platform_state="connected")
assert _platforms(home)["alpha:telegram"]["state"] == "fatal"
# User deletes the copied TELEGRAM_BOT_TOKEN line: the .env change triggers a rescan.
(alpha / ".env").write_text("DISCORD_BOT_TOKEN=alpha-token\n# edited\n", encoding="utf-8")
assert (await runner.reconcile_served_profiles())["rescanned"] == ["alpha"]
platforms = _platforms(home)
assert "alpha:telegram" not in platforms
assert platforms["alpha:discord"]["state"] == "connected"
assert platforms["telegram"]["state"] == "connected"