fix(desktop): yield single-profile cron to its running gateway

(cherry picked from commit db92ff258f0138a251b2c4e14b32f2fa0b7b8d5b)
This commit is contained in:
tachyon-r
2026-09-07 05:55:35 -04:00
committed by Teknium
parent c17629a0a2
commit 32c538851a
3 changed files with 40 additions and 14 deletions

View File

@@ -100,7 +100,9 @@ def _start_desktop_cron_ticker(stop_event: "threading.Event", interval: int = 60
from hermes_cli.profiles import profiles_to_serve
profile_homes = list(profiles_to_serve(multiplex=True))
if len(profile_homes) > 1:
if profile_homes:
# Even one profile needs the per-tick gateway gate; otherwise
# Desktop races its dedicated gateway for the same cron store.
start_kwargs["profile_homes"] = profile_homes
# Stand down, per tick, for a profile whose OWN gateway runs:
# it ticks with live adapters, and the tick-lock race would

View File

@@ -15,6 +15,8 @@ import asyncio
import threading
from unittest.mock import patch
import pytest
def test_standalone_fallback_pool_keeps_profile_scope(tmp_path, monkeypatch):
@@ -105,16 +107,18 @@ def test_multiplex_ticker_profile_gate_skips_rejected_profile(tmp_path):
assert (orphan / "cron" / "ticker_last_success").exists()
def test_desktop_ticker_gates_on_profile_gateway_running(tmp_path, monkeypatch):
"""The desktop ticker wires the gate to ``_check_gateway_running``."""
@pytest.mark.parametrize("profile_count", [1, 2])
def test_desktop_ticker_gates_on_profile_gateway_running(tmp_path, monkeypatch, profile_count):
"""Desktop yields to each live gateway, including a single-profile install."""
from hermes_cli import web_server
homes = [("default", tmp_path / "default"), ("ops", tmp_path / "ops")]
homes = [("default", tmp_path / "default"), ("ops", tmp_path / "ops")][:profile_count]
running = {homes[-1][1]}
monkeypatch.setattr(
"hermes_cli.profiles.profiles_to_serve", lambda multiplex=False: list(homes)
)
monkeypatch.setattr(
"hermes_cli.profiles._check_gateway_running", lambda home: home.name == "ops"
"hermes_cli.profiles._check_gateway_running", lambda home: home in running
)
captured = {}
@@ -133,7 +137,15 @@ def test_desktop_ticker_gates_on_profile_gateway_running(tmp_path, monkeypatch):
web_server._start_desktop_cron_ticker(threading.Event(), interval=0)
assert captured.get("profile_homes") == homes
gate = captured.get("profile_gate")
assert gate is not None, "desktop ticker did not install a profile gate"
assert gate("default", tmp_path / "default") is True
assert gate("ops", tmp_path / "ops") is False
for name, home in homes:
assert gate(name, home) is (home not in running)
# Re-evaluate on each tick: Desktop resumes fallback after gateway exit
# and stands down again if a gateway starts later.
running.clear()
assert all(gate(name, home) for name, home in homes)
running.update(home for _, home in homes)
assert not any(gate(name, home) for name, home in homes)

View File

@@ -70,19 +70,31 @@ def test_multi_profile_homes_passed_to_builtin(monkeypatch, _providers, tmp_path
assert builtin.start_kwargs["profile_homes"] == homes
def test_single_profile_keeps_legacy_path(monkeypatch, _providers, tmp_path):
_sp, builtin = _providers
@pytest.mark.parametrize("gateway_running", [True, False])
def test_single_profile_ticks_only_without_gateway(monkeypatch, tmp_path, gateway_running):
"""Exercise Desktop startup through the real built-in scheduler loop."""
from cron.scheduler_provider import InProcessCronScheduler
from hermes_constants import get_hermes_home
import hermes_cli.profiles as profiles_mod
home = tmp_path / "root"
home.mkdir()
monkeypatch.setattr(profiles_mod, "profiles_to_serve", lambda **_kw: [("default", home)])
monkeypatch.setattr(profiles_mod, "_check_gateway_running", lambda _home: gateway_running)
monkeypatch.setattr(
profiles_mod,
"profiles_to_serve",
lambda **_kw: [("default", tmp_path / "root")],
"cron.scheduler_provider.resolve_cron_scheduler", InProcessCronScheduler
)
ticked = []
monkeypatch.setattr(
"cron.scheduler.tick", lambda **_kw: ticked.append(get_hermes_home())
)
stop = threading.Event()
# One real scheduler cycle, with no wall-clock wait or job dispatch.
monkeypatch.setattr(stop, "wait", lambda _timeout: stop.set())
ws._start_desktop_cron_ticker(threading.Event(), interval=9)
ws._start_desktop_cron_ticker(stop, interval=0)
assert builtin.start_kwargs == {"interval": 9}
assert ticked == ([] if gateway_running else [home])
def test_enumeration_failure_fails_open(monkeypatch, _providers):