fix(gateway): preserve profile config changes during connection
This commit is contained in:
@@ -827,6 +827,7 @@ class GatewayAdapterLifecycleMixin:
|
||||
Each profile connects under its own HERMES_HOME + secret scope; credential/listener collisions
|
||||
are refused here — the only point seeing every profile's credentials together."""
|
||||
from gateway.run import MultiplexConfigError, _multiplex_profile_homes
|
||||
from gateway.run_profile_reconcile import profile_serve_signature
|
||||
if not self._multiplex_on():
|
||||
return 0
|
||||
try:
|
||||
@@ -837,9 +838,12 @@ class GatewayAdapterLifecycleMixin:
|
||||
connected = 0
|
||||
claimed = self._primary_resource_claims(active)
|
||||
profile_homes = _multiplex_profile_homes(self.config)
|
||||
self._served_profile_signatures = {}
|
||||
for profile_name, profile_home in profile_homes:
|
||||
if profile_name == active:
|
||||
continue # handled by the primary startup loop
|
||||
# Preserve changes made while the initial connection is awaiting I/O.
|
||||
self._served_profile_signatures[profile_name] = profile_serve_signature(profile_home)
|
||||
try:
|
||||
connected += await self._start_one_profile_adapters(profile_name, profile_home, claimed)
|
||||
except MultiplexConfigError:
|
||||
|
||||
@@ -116,6 +116,9 @@ class GatewayProfileReconcileMixin:
|
||||
result["removed"].append(name)
|
||||
claimed = self._live_resource_claims(active)
|
||||
for name in added + changed:
|
||||
# Only acknowledge the configuration observed before connecting;
|
||||
# a setup save during an awaited handshake needs another scan.
|
||||
scan_signature = profile_serve_signature(current[name])
|
||||
try:
|
||||
connected = await self._start_one_profile_adapters(name, current[name], claimed)
|
||||
except MultiplexConfigError as exc:
|
||||
@@ -125,7 +128,7 @@ class GatewayProfileReconcileMixin:
|
||||
except Exception:
|
||||
logger.error("[MULTIPLEX] Failed to start adapters for profile '%s'", name, exc_info=True)
|
||||
connected = 0
|
||||
sigs[name] = profile_serve_signature(current[name])
|
||||
sigs[name] = scan_signature
|
||||
if name in added:
|
||||
logger.info("[MULTIPLEX] Now serving profile '%s' (%s adapter(s) connected; %s)", name, connected, reason)
|
||||
result["added"].append(name)
|
||||
|
||||
73
tests/gateway/test_profile_signature_during_connect.py
Normal file
73
tests/gateway/test_profile_signature_during_connect.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""A config saved while a profile connects must remain pending for the next scan."""
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import GatewayConfig, Platform
|
||||
from gateway.run import GatewayRunner
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("startup", [False, True])
|
||||
async def test_config_saved_during_connect_is_rescanned(tmp_path, monkeypatch, startup):
|
||||
home = tmp_path / ".hermes"
|
||||
profile = home / "profiles" / "worker"
|
||||
profile.mkdir(parents=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.profiles.get_active_profile_name", lambda: "default"
|
||||
)
|
||||
(profile / "config.yaml").write_text("model: {default: test}\n", encoding="utf-8")
|
||||
secrets = profile / ".env"
|
||||
secrets.write_text("DISCORD_BOT_TOKEN=discord-test\n", encoding="utf-8")
|
||||
runner = object.__new__(GatewayRunner)
|
||||
runner.config = GatewayConfig(multiplex_profiles=True)
|
||||
runner._running = True
|
||||
runner._primary_profile_name = "default"
|
||||
(
|
||||
runner.adapters,
|
||||
runner._profile_adapters,
|
||||
runner._failed_platforms,
|
||||
runner._profile_failed_platforms,
|
||||
) = {}, {}, {}, {}
|
||||
runner.pairing_store, runner.pairing_stores = MagicMock(), {}
|
||||
runner._busy_text_modes_by_profile, runner._busy_input_modes_by_profile = {}, {}
|
||||
runner._register_config_hooks = lambda *a, **kw: None
|
||||
runner._configure_profile_adapter = lambda *a: None
|
||||
runner._sync_voice_mode_state_to_adapter = lambda *a: None
|
||||
runner._restore_secondary_completion_ledgers = lambda *a: None
|
||||
runner._adapter_credential_claim = lambda *a: None
|
||||
runner._adapter_listener_claim = lambda *a: None
|
||||
runner._create_adapter = lambda platform, config: SimpleNamespace(platform=platform)
|
||||
runner._note_served_profiles([("default", home)])
|
||||
connected = []
|
||||
|
||||
async def connect(adapter, platform):
|
||||
connected.append(platform)
|
||||
if platform == Platform.DISCORD:
|
||||
# Configuration was already read; a second setup operation finishes while
|
||||
# the first adapter is awaiting its transport handshake.
|
||||
secrets.write_text(
|
||||
"DISCORD_BOT_TOKEN=discord-test\nTELEGRAM_BOT_TOKEN=telegram-test\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return True
|
||||
|
||||
async def after_added(profiles):
|
||||
pass
|
||||
|
||||
runner._connect_initial_adapter_with_timeout = connect
|
||||
runner._after_profiles_added = after_added
|
||||
if startup:
|
||||
await runner._start_secondary_profile_adapters()
|
||||
else:
|
||||
await runner.reconcile_served_profiles()
|
||||
assert connected == [Platform.DISCORD]
|
||||
await runner.reconcile_served_profiles()
|
||||
assert connected == [Platform.DISCORD, Platform.TELEGRAM]
|
||||
await runner.reconcile_served_profiles()
|
||||
assert connected == [Platform.DISCORD, Platform.TELEGRAM]
|
||||
Reference in New Issue
Block a user