fix(s6): register profile gateways without auto-starting (#46266)

* fix(s6): prevent profile create from auto-starting gateway service

When hermes profile create runs inside an s6 container,
_maybe_register_gateway_service() calls register_profile_gateway()
which creates the service directory and triggers s6-svscanctl -a.
Previously the service always started immediately, causing profiles
that share the main gateway's bot token (e.g. Kanban worker profiles)
to fail with a token-lock conflict and persist gateway_state: running
— becoming zombies that resurrect on every container restart.

Wire the existing start_now parameter through the S6 implementation:
when start_now=False, write a  marker file (same pattern as
container_boot.py _register_gateway_slot) so s6-supervise leaves the
service stopped until the user explicitly runs hermes -p <profile>
gateway start.

4 files, +61/-6, 4 new tests (all passing).

* test(docker): wait for gateway running state before restart

---------

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
This commit is contained in:
Teknium
2026-06-15 11:43:23 +10:00
committed by GitHub
co-authored by liuhao1024
parent 4eb0ff639b
commit 40d7c264f0
5 changed files with 63 additions and 7 deletions
+2 -1
View File
@@ -296,7 +296,8 @@ def test_live_gateway_autostarts_after_real_restart_without_manual_state_stamp(
)
if r.returncode == 0 and '"gateway_state"' in r.stdout:
state = r.stdout
break
if '"running"' in state:
break
time.sleep(0.5)
assert '"running"' in state, (
f"gateway never persisted running state pre-restart: {state!r}"
@@ -54,10 +54,12 @@ class _S6Manager:
def register_profile_gateway(
self, profile: str, *,
extra_env: dict[str, str] | None = None,
start_now: bool = True,
) -> None:
if self.raise_on_register is not None:
raise self.raise_on_register
self.registered.append(profile)
self.last_start_now = start_now
def unregister_profile_gateway(self, profile: str) -> None:
if self.raise_on_unregister is not None:
@@ -107,6 +109,21 @@ def test_register_calls_through_on_s6(monkeypatch: pytest.MonkeyPatch) -> None:
assert mgr.registered == ["coder"]
def test_register_passes_start_now_false(monkeypatch: pytest.MonkeyPatch) -> None:
"""_maybe_register_gateway_service must register with start_now=False
so that profile creation does not auto-start a gateway that may
conflict with the main gateway's bot-token lock."""
_patch_detect_s6(monkeypatch)
mgr = _S6Manager()
monkeypatch.setattr(
"hermes_cli.service_manager.get_service_manager", lambda: mgr,
)
_maybe_register_gateway_service("coder")
assert mgr.last_start_now is False, (
"profile creation must not auto-start the gateway service"
)
def test_register_swallows_duplicate_value_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
+29
View File
@@ -586,6 +586,35 @@ def test_s6_register_creates_service_dir_and_triggers_scan(
), f"s6-svscanctl -a not invoked; saw: {fake_subprocess_run}"
def test_s6_register_start_now_false_writes_down_marker(
s6_scandir, fake_subprocess_run,
) -> None:
"""When start_now=False, a `down` marker must be written so
s6-supervise does not auto-start the service on rescan."""
mgr = S6ServiceManager(scandir=s6_scandir)
mgr.register_profile_gateway("coder", start_now=False)
svc_dir = s6_scandir / "gateway-coder"
assert svc_dir.is_dir()
assert (svc_dir / "down").is_file(), (
"start_now=False must write a `down` marker file"
)
def test_s6_register_start_now_true_no_down_marker(
s6_scandir, fake_subprocess_run,
) -> None:
"""When start_now=True (default), no `down` marker should exist."""
mgr = S6ServiceManager(scandir=s6_scandir)
mgr.register_profile_gateway("coder")
svc_dir = s6_scandir / "gateway-coder"
assert svc_dir.is_dir()
assert not (svc_dir / "down").exists(), (
"start_now=True must NOT write a `down` marker file"
)
def test_s6_register_extra_env_is_quoted(s6_scandir, fake_subprocess_run) -> None:
mgr = S6ServiceManager(scandir=s6_scandir)
mgr.register_profile_gateway(